Welcome to pypragma’s documentation!

Collapse Literals

Collapse literal operations in code to their results, e.g. x = 1 + 2 gets converted to x = 3.

For example:

@pragma.collapse_literals
def f(y):
    x = 3
    return x + 2 + y

# ... Becomes ...

def f(y):
    x = 3
    return 5 + y

This is capable of resolving expressions of numerous sorts:

  • A variable with a known value is replaced by that value
  • An iterable with known values (such as one that could be unrolled by pragma.unroll()), if indexed, is replaced with the value at that location
  • A unary, binary, or logical operation on known values is replaced by the result of that operation on those values
  • A if/elif/else block is trimmed of options that are known at decoration-time to be impossible. If it can be known which branch runs at decoration time, then the conditional is removed altogether and replaced with the body of that branch

If a branch is constant, and thus known at decoration time, then only the correct branch will be left:

@pragma.collapse_literals
def f():
    x = 1
    if x > 0:
        x = 2
    return x

# ... Becomes ...

def f():
    x = 1
    x = 2
    return 2

This decorator is actually very powerful, understanding any definition-time known collections, primitives, or even dictionaries. Subscripts are resolved if the list or dictionary, and the key into it, can be resolved. Names are replaced by their values if they are not containers (since re-writing a container, such as a tuple or list, could duplicate object references). Functions, such as len and sum can be computed and replaced with their value if their arguments are known.

Only primitive types are resolved, and this does not include iterable types. To control this behavior, use the collapse_iterables argument. Example:

v = [1, 2]

@pragma.collapse_literals
def f():
    yield v

# ^ nothing happens ^

@pragma.collapse_literals(collapse_iterables=True)
def f():
    yield v

# ... Becomes ...

def f():
    yield [1, 2]

There are cases where you don’t want to collapse all literals. It often happens when you have lots of global variables and long functions, or if you want to apply different pragma patterns to different parts of the function. Fine control is possible with the explicit_only argument. When True, only explicit keyword arguments and the value of the function_globals argument (itself a dictionary) are collapsed.

pragma is capable of logical and mathematical deduction, meaning that expressions with unknowns can be collapsed if the known elements determine the result. For example, False and anything is logically equivalent to False. True or anything is always True. Mathematical: anything ** 0 -> 1. 0 * anything -> 0.

Todo

Always commit changes within a block, and only mark values as non-deterministic outside of conditional blocks

Todo

Support list/set/dict comprehensions

Todo

Attributes are too iffy, since properties abound, but assignment to a known index of a known indexable should be remembered

De-index Arrays

Convert literal indexing operations for a given array into named value references. The new value names are de-indexed and stashed in the function’s closure so that the resulting code both uses no literal indices and still behaves as if it did. Variable indices are unaffected.

For example:

v = [object(), object(), object()]

@pragma.deindex(v, 'v')
def f(x):
    yield v[0]
    yield v[x]

# ... f becomes ...

def f(x):
    yield v_0  # This is defined as v_0 = v[0] by the function's closure
    yield v[x]

# We can check that this works correctly
assert list(f(2)) == [v[0], v[2]]

This can be easily stacked with pragma.unroll() to unroll iterables in a function when their values are known at function definition time:

funcs = [lambda x: x, lambda x: x ** 2, lambda x: x ** 3]

@pragma.deindex(funcs, 'funcs')
@pragma.unroll(lf=len(funcs))
def run_func(i, x):
    for j in range(lf):
        if i == j:
            return funcs[j](x)

# ... Becomes ...

def run_func(i, x):
    if i == 0:
        return funcs_0(x)
    if i == 1:
        return funcs_1(x)
    if i == 2:
        return funcs_2(x)

This could be used, for example, in a case where dynamically calling functions isn’t supported, such as in numba.jit or numba.cuda.jit.

Note that because the array being de-indexed is passed to the decorator, the value of the constant-defined variables (e.g. v_0 in the code above) is “compiled” into the code of the function, and won’t update if the array is updated. Again, variable-indexed calls remain unaffected.

Since names are (and must) be used as references to the array being de-indexed, it’s worth noting that any other local variable of the format "{iterable_name}_{i}" will get shadowed by this function. The string passed to iterable_name must be the name used for the iterable within the wrapped function.

Unroll

Unroll constant loops. If the for-loop iterator is a known value at function definition time, then replace it with its body duplicated for each value. For example:

def f():
for i in [1, 2, 4]:
    yield i

could be identically replaced by:

def f():
    yield 1
    yield 2
    yield 4

The unroll decorator accomplishes this by parsing the input function, performing the unrolling transformation on the function’s AST, then compiling and returning the defined function.

unroll is currently smart enough to notice literal defined variables and literals, as well as able to unroll the range function and unroll nested loops:

@pragma.unroll
def summation(x=0):
    a = [x, x, x]
    v = 0
    for _a in a:
        v += _a
    return v

# ... Becomes ...

def summation(x=0):
    a = [x, x, x]
    v = 0
    v += x
    v += x
    v += x
    return v

# ... But ...

@pragma.unroll
def f():
    x = 3
    for i in [x, x, x]:
        yield i
    x = 4
    a = [x, x, x]
    for i in a:
        yield i

# ... Becomes ...

def f():
    x = 3
    yield 3
    yield 3
    yield 3
    x = 4
    a = [x, x, x]
    yield 4
    yield 4
    yield 4

# Even nested loops and ranges work!

@pragma.unroll
def f():
    for i in range(3):
        for j in range(3):
            yield i + j

# ... Becomes ...

def f():
    yield 0 + 0
    yield 0 + 1
    yield 0 + 2
    yield 1 + 0
    yield 1 + 1
    yield 1 + 2
    yield 2 + 0
    yield 2 + 1
    yield 2 + 2

unroll also supports tuple-target interation with enumerate, zip, and items:

v = [1, 3, 5]

@pragma.unroll
def f():
    for i, elem in enumerate(v):
        yield i, elem

# ... Becomes ...

def f():
    yield 0, 1
    yield 1, 3
    yield 2, 5

Partial unrolling and targeted unrolling are supported. unroll_targets lets you explicitly specify which loops should be unrolled. This is useful in functions with several loops that should behave differently. unroll_in_tiers is a performance measure for reducing overhead in loop calls. It is a tuple of (iterable_name, length_of_loop, number_of_inner_iterations), where iterable_name specifies what to unroll, length_of_loop is how many iterations in total, and number_of_inner_iterations is the number of explicit repetitions of the inside of the loop before reaching the end of the new loop.

a = list(range(0, 7))

@pragma.unroll(unroll_in_tiers=('PRAGMArange', len(a), 2))
def f():
    for i in PRAGMArange:
        yield a[i]

# ... Becomes ...

def f():
    for PRAGMA_iouter in range(0, 6, 2):
        yield a[PRAGMA_iouter]
        yield a[PRAGMA_iouter + 1]
    yield a[6]

In that example, pragma handled a single remainder call because the length of the iterable was odd, while the step was 2.

When combined with deindex, unroll can also handle cases where the values being iterated over are not literals. The decorators must be in this order (deindex being applied before unroll), and the collapse_iterables argument is necessary:

d = {'a': object(), 'b': object()}

@pragma.unroll
@pragma.deindex(d, 'd', collapse_iterables=True)
def f():
    for k, v in d.items():
        yield k
        yield v

# ... Becomes ...

def f():
    yield 'a'
    yield d_a
    yield 'b'
    yield d_b

Also supported are recognizing top-level breaks. Breaks inside conditionals aren’t yet supported, though they could eventually be by combining unrolling with literal condition collapsing:

@pragma.unroll
def f(y):
    for i in range(100000):
        for x in range(2):
            if i == y:
                break
        break

# ... Becomes ...

def f(y):
    for x in range(2):
        if 0 == y:
            break

Todo

Assignment to known lists and dictionaries

Todo

Resolving compile-time known conditionals before detecting top-level breaks

Inlining Functions

Inline specified functions into the decorated function. Unlike in C, this directive is placed not on the function getting inlined, but rather the function into which it’s getting inlined (since that’s the one whose code needs to be modified and hence decorated). Currently, this is implemented in the following way:

  • When a function is called, its call code is placed within the current code block immediately before the line where its value is needed
  • The code is wrapped in a one-iteration for loop (effectively a do {} while(0)), and the return statement is replaced by a break
  • Arguments are stored into a dictionary, and variadic keyword arguments are passed as dict_name.update(kwargs); this dictionary has the name _[funcname] where funcname is the name of the function being inlined, so other variables of this name should not be used or relied upon
  • The return value is assigned to the function name as well, deleting the argument dictionary, freeing its memory, and making the return value usable when the function’s code is exited by the break
  • The call to the function is replaced by the variable holding the return value

As a result, pragma.inline cannot currently handle functions which contain a return statement within a loop. Since Python doesn’t support anything like goto besides wrapping the code in a function (which this function implicitly shouldn’t do), I don’t know how to surmount this problem. Without much effort, it can be overcome by tailoring the function to be inlined.

To inline a function f into the code of another function g, use pragma.inline(g)(f), or, as a decorator:

def f(x):
    return x**2

@pragma.inline(f)
def g(y):
    z = y + 3
    return f(z * 4)

# ... g Becomes something like ...

def g(y):
    z = y + 3
    _f = dict(x=z * 4)  # Store arguments
    for ____ in [None]:  # Function body
        _f['return'] = _f['x'] ** 2  # Store the "return"ed value
        break  # Return, terminate the function body
    _f_return = _f.get('return', None)  # Retrieve the returned value
    del _f  # Discard everything else
    return _f_return

This loop can be removed, if it’s not necessary, using :func:pragma.unroll. This can be accomplished if there are no returns within a conditional or loop block. In this case:

def f(x):
    return x**2

@pragma.unroll
@pragma.inline(f)
def g(y):
    z = y + 3
    return f(z * 4)

# ... g Becomes ...

def g(y):
    z = y + 3
    _f = {}
    _f['x'] = z * 4
    _f = _f['x'] ** 2
    return _f

It needs to be noted that, besides arguments getting stored into a dictionary, other variable names remain unaltered when inlined. Thus, if there are shared variable names in the two functions, they might overwrite each other in the resulting inlined function.

Todo

Fix name collision by name-mangling non-free variables

Eventually, this could be collapsed using :func:pragma.collapse_literals, to produce simply return ((y + 3) * 4) ** 2, but dictionaries aren’t yet supported for collapsing.

When inlining a generator function, the function’s results are collapsed into a list, which is then returned. This will break in two main scenarios:

  • The generator never ends, or consumes excessive amounts of resources.
  • The calling code relies on the resulting generator being more than just iterable.

In general, either this won’t be an issue, or you should know better than to try to inline the infinite generator.

Todo

Support inlining a generator into another generator by merging the functions together. E.g., for x in my_range(5): yield x + 2 becomes i = 0; while i < 5: yield i + 2; i += 1 (or something vaguely like that).

Lambda Lift

Lifts a function out of its environment to convert it into a pure function. This is accomplished by converting all free variables into keyword-only arguments. This works best on closures, where free variables can be automatically detected (Python stores them with the function object), but global variables can also be explicitly lifted as well.

For example, consider the following closure:

def make_f(x):
    def f(y):
        return x + y
    return f

my_f = make_f(5)
my_f(3)  # 8

Closures are handy programming tools, but are not purely functional and hence can cause issues with code generators. Converting the closure into a pure function is relatively simple, by simply replacing all free variables with parameters. For example, the above code could be converted to:

def f(y, *, x):
    return x + y

f(3, x=5)

There are minor quirks to this process in Python to handle global variables and imports (both of which are mutable state around the function, but aren’t necessarily labelled as “free variables”), but the essential process remains the same. pragma.lift() enables the above tranformation easily, either when the closure is created, or once it has been obtained:

In [1]: def make_f(x):
   ...:     @pragma.lift(imports=False)
   ...:     def f(y):
   ...:         return x + y
   ...:     return f
   ...:

In [2]: my_f = make_f(5)

In [3]: my_f??
Signature: my_f(y, *, x)
Source:
def f(y, *, x):
    return x + y

Note that, by default, lift attempts to return the simplest possible function that mimics the wrapped function while including all closure variables as arguments. However, several features are available to produce more useful and transparent pure functions. These features will be discussed below.

Defaults and Annotations

It should be noticed that, in the above example, the produced function f requires that x be provided on every function call. While this makes the function pure and free of its closure, perhaps we want to infer some information from the closure to simplify the use of the produced pure function. By using the value of the free variable in the function’s closure, we can infer the variable’s default value and general type, if desired. For example, the above closure could also have been rewritten as the following pure function:

def f(y, *, x=3): ...

Or even more specifically as:

def f(y, *, x: int=3): ...

If the variable’s value can be converted into a Python literal, and if its type can be converted to a string, then its default value and type annotation, respectively, may be added by pragma.lift() at decoration time:

In [1]: def make_f(x):
   ...:     @pragma.lift(defaults=True, annotate_types=True, imports=False)
   ...:     def f(y):
   ...:         return x + y
   ...:     return f
   ...:

In [2]: f = make_f(5)

In [3]: f??
Signature: f(y, *, x:int=5)
Source:
def f(y, *, x: int=5):
    return x + y

Additionally, both defaults and annotate_types can take a list to selectively apply to certain free variables:

In [1]: def make_g(x, y):
   ...:     @pragma.lift(defaults=['x'], annotate_types=['y'], imports=False)
   ...:     def g(z):
   ...:         return x + y + z
   ...:     return g
   ...:

In [2]: g = make_g(1, 2)

In [3]: g??
Signature: g(z, *, x=1, y:int)
Source:
def g(z, *, x=1, y: int):
    return x + y + z

If complete control is needed, these may also be dictionaries, where the key is the free variable name. defaults requires that the value of the dictionary entry, if it exists, must be a Python literal or any ast.AST expression (ast.expr). For annotate_types, the value of the dictionary entry, if it exists, must be a string or ast.AST expression (ast.expr).

Globals

Python does not annotate free variables that are available in the function’s global context (versus its closure). This information might theoretically be statically extracted from the function’s code, it is safest simply to require this to be specified explicitly at decoration time. This is done using the lift_globals list:

x = 5

@pragma.lift(lift_globals=['x'], imports=False)
def f(y):
    return x + y

f(7, x=7)  # 14

Imports

For the produced function to be truly functional (as much as can be in Python), it cannot rely on its global environment at all. Most practical functions, however, rely on imported modules, which are often imported at the module level. Re-writing a function to contain all of its own needed imports is tedious and prone to accidentally using globally imported modules anyway. To make this utility practical, by default it finds all imports in the global and closure context and includes them within the function. The performance impact of this should be minimal, since module imports are cached in Python. If imports are not suppressed like in the above examples, module imports are added to the top of the function’s code (respecting the docstring, if any):

In [1]: import pragma
   ...: import sys
   ...:
   ...: @pragma.lift
   ...: def f():
   ...:     return sys.version_info
   ...:
f
In [2]: f??
Signature: f()
Source:
def f():
    import pragma
    import sys
    return sys.version_info

Note

Any imported objects that aren’t modules, such as functions, classes, or shared variables, aren’t automatically imported since they are indistinguishable from being just another global variable. These must be included in the lift_globals argument list.

Note that, just like global variables, global imports can’t be checked for necessity and so are universally included. Which modules get imported can be filtered by passing a list to imports:

In [1]: import pragma
   ...: import sys
   ...:
   ...: @pragma.lift(imports=['sys'])
   ...: def f():
   ...:     return sys.version_info
   ...:

In [2]: f??
Signature: f()
Source:
def f():
    import sys
    return sys.version_info

TODO List

Todo

Replace custom stack implementation with collections.ChainMap

Todo

Implement decorator to eliminate unused lines of code (assignments to unused values)

Todo

Technically, x += y doesn’t have to be the same thing as x = x + y. Handle it as its own operation of the form x += y; return x

Todo

Support efficiently inlining simple functions, i.e. where there is no return or only one return as the last line of the function, using pure name substitution without loops, try/except, or anything else fancy

Todo

Catch replacement of loop variables that conflict with globals, or throw a more descriptive error when detected. See test_iteration_variable

Todo

Python 3.8/3.9+ support: https://docs.python.org/3/library/ast.html . ast.Constant is taking over for ast.[Num, Str, Bytes, NameConstant, Ellipsis]. Simple-valued indexes are now values, and extended slices are now tuples: ast.[Index, ExtSlice] no longer exist.

Todo

Replace custom stack implementation with collections.ChainMap

(The original entry is located in /home/docs/checkouts/readthedocs.org/user_builds/pypragma/checkouts/latest/docs/todo.rst, line 4.)

Todo

Implement decorator to eliminate unused lines of code (assignments to unused values)

(The original entry is located in /home/docs/checkouts/readthedocs.org/user_builds/pypragma/checkouts/latest/docs/todo.rst, line 5.)

Todo

Technically, x += y doesn’t have to be the same thing as x = x + y. Handle it as its own operation of the form x += y; return x

(The original entry is located in /home/docs/checkouts/readthedocs.org/user_builds/pypragma/checkouts/latest/docs/todo.rst, line 6.)

Todo

Support efficiently inlining simple functions, i.e. where there is no return or only one return as the last line of the function, using pure name substitution without loops, try/except, or anything else fancy

(The original entry is located in /home/docs/checkouts/readthedocs.org/user_builds/pypragma/checkouts/latest/docs/todo.rst, line 7.)

Todo

Catch replacement of loop variables that conflict with globals, or throw a more descriptive error when detected. See test_iteration_variable

(The original entry is located in /home/docs/checkouts/readthedocs.org/user_builds/pypragma/checkouts/latest/docs/todo.rst, line 8.)

Todo

Python 3.8/3.9+ support: https://docs.python.org/3/library/ast.html . ast.Constant is taking over for ast.[Num, Str, Bytes, NameConstant, Ellipsis]. Simple-valued indexes are now values, and extended slices are now tuples: ast.[Index, ExtSlice] no longer exist.

(The original entry is located in /home/docs/checkouts/readthedocs.org/user_builds/pypragma/checkouts/latest/docs/todo.rst, line 9.)

Todo

Always commit changes within a block, and only mark values as non-deterministic outside of conditional blocks

(The original entry is located in /home/docs/checkouts/readthedocs.org/user_builds/pypragma/checkouts/latest/docs/collapse_literals.rst, line 69.)

Todo

Support list/set/dict comprehensions

(The original entry is located in /home/docs/checkouts/readthedocs.org/user_builds/pypragma/checkouts/latest/docs/collapse_literals.rst, line 70.)

Todo

Attributes are too iffy, since properties abound, but assignment to a known index of a known indexable should be remembered

(The original entry is located in /home/docs/checkouts/readthedocs.org/user_builds/pypragma/checkouts/latest/docs/collapse_literals.rst, line 71.)

Todo

Fix name collision by name-mangling non-free variables

(The original entry is located in /home/docs/checkouts/readthedocs.org/user_builds/pypragma/checkouts/latest/docs/inline.rst, line 60.)

Todo

Support inlining a generator into another generator by merging the functions together. E.g., for x in my_range(5): yield x + 2 becomes i = 0; while i < 5: yield i + 2; i += 1 (or something vaguely like that).

(The original entry is located in /home/docs/checkouts/readthedocs.org/user_builds/pypragma/checkouts/latest/docs/inline.rst, line 71.)

Todo

Assignment to known lists and dictionaries

(The original entry is located in /home/docs/checkouts/readthedocs.org/user_builds/pypragma/checkouts/latest/docs/unroll.rst, line 161.)

Todo

Resolving compile-time known conditionals before detecting top-level breaks

(The original entry is located in /home/docs/checkouts/readthedocs.org/user_builds/pypragma/checkouts/latest/docs/unroll.rst, line 162.)

Overview

PyPragma is a set of tools for performing in-place code modification, inspired by compiler directives in C. These modifications are intended to make no functional changes to the execution of code. In C, this is used to increase code performance or make certain tradeoffs (often between the size of the binary and its execution speed). In Python, these changes are most applicable when leveraging code generation libraries (such as Numba or Tangent) where the use of certain Python features is disallowed. By transforming the code in-place, disallowed features can be converted to allowed syntax at runtime without sacrificing the dynamic nature of python code.

For example, with Numba, it is not possible to compile a function which dynamically references and calls other functions (e.g., you may not select a function from a list and then execute it, you may only call functions by their explicit name):

fns = [sin, cos, tan]

@numba.jit
def call(i, x):
   return fns[i](x)  # Not allowed, since it's unknown which function is getting called

If the dynamism is static by the time the function is defined, such as in this case, then these dynamic language features can be flattened to simpler features that such code generation libraries are more likely to support (e.g., the function can be extracted into a closure variable, then called directly by that name):

fns = [sin, cos, tan]

fns_0 = fns[0]
fns_1 = fns[1]
fns_2 = fns[2]

@numba.jit
def call(i, x):
   if i == 0:
      return fns_0(x)
   if i == 1:
      return fns_1(x)
   if i == 2:
      return fns_2(x)

Such a modification can only be done by the programmer if the dynamic features are known before runtime, that is, if fns is dynamically computed, then this modification cannot be performed by the programmer, even though this example demonstrates the the original function is not inherently dynamic, it just appears so. PyPragma enables this transformation at runtime, which for this example function would look like:

fns = [sin, cos, tan]

@numba.jit
@pragma.deindex(fns, 'fns')
@pragma.unroll(num_fns=len(fns))
def call(i, x):
   for j in range(num_fns):
      if i == j:
         return fns[j](x)  # Still dynamic call, but decorators convert to static

This example is converted, in place and at runtime, to exactly the unrolled code above.

Installation

As usual, you have the choice of installing from PyPi:

pip install pragma

or directly from Github:

pip install git+https://github.com/scnerd/pypragma

Usage

PyPragma has a small number of stackable decorators, each of which transforms a function in-place without changing its execution behavior. These can be imported as such:

import pragma

Each decorator can be applied to a function using either the standard decorator syntax, or as a function call:

@pragma.unroll
def pows(i):
   for x in range(3):
      yield i ** x

pows(5)

# Is identical to...

def pows(i):
   for x in range(3):
      yield i ** x

pragma.unroll(pows)(5)

# Both of which become...

def pows(i):
   yield i ** 0
   yield i ** 1
   yield i ** 2

pows(5)

Each decorator can be used bare, as in the example above, or can be given initial parameters before decorating the given function. Any non-specified keyword arguments are added to the resulting function’s closure as variables. In addition, the decorated function’s closure is preserved, so external variables are also included. As a simple example, the above code could also be written as:

@pragma.unroll(num_pows=3)
def pows(i):
   for x in range(num_pows):
      yield i ** x

# Or...

num_pows = 3
@pragma.unroll
def pows(i):
   for x in range(num_pows):
      yield i ** x

Certain keywords are reserved, of course, as will be defined in the documentation for each decorator. Additionally, the resulting function is an actual, proper Python function, and hence must adhere to Python syntax rules. As a result, some modifications depend upon using certain variable names, which may collide with other variable names used by your function. Every effort has been made to make this unlikely by using mangled variable names, but the possibility for collision remains.

A side effect of the proper Python syntax is that functions can have their source code retrieved by any normal Pythonic reflection:

In [1]: @pragma.unroll(num_pows=3)
  ...: def pows(i):
  ...:    for x in range(num_pows):
  ...:       yield i ** x
  ...:

In [2]: pows??
Signature: pows(i)
Source:
def pows(i):
    yield i ** 0
    yield i ** 1
    yield i ** 2
File:      /tmp/tmpmn5bza2j
Type:      function

In general, all decorators consider a value to be known if:

  • It is defined in the function’s closure, and is not modified earlier in the function
  • It is defined as a keyword global to the decorator, and is not modified earlier in the function
  • It is a literal
  • It is a variable assigned a literal earlier in the function (even if it overrides a previously known value)
  • It is a known index into a known list or tuple (dictionaries are not yet supported)

Some special cases include:

  • In collapse_literals, any operation on known values gets reduced to a known value

Additionally, as a utility primarily for testing and debugging, the source code can be easily retrieved from each decorator instead of the transformed function by using the return_source=True argument.

Quick Examples

Collapse Literals

Complete documentation:

In [1]: @pragma.collapse_literals(x=5)
   ...: def f(y):
   ...:     z = x // 2
   ...:     return y * 10**z
   ...:

In [2]: f??
Signature: f(y)
Source:
def f(y):
    z = 2
    return y * 100

De-index Arrays

Complete documentation:

In [1]: fns = [math.sin, math.cos, math.tan]

In [2]: @pragma.deindex(fns, 'fns')
   ...: def call(i, x):
   ...:     if i == 0:
   ...:         return fns[0](x)
   ...:     if i == 1:
   ...:         return fns[1](x)
   ...:     if i == 2:
   ...:         return fns[2](x)
   ...:

In [3]: call??
Signature: call(i, x)
Source:
def call(i, x):
    if i == 0:
        return fns_0(x)
    if i == 1:
        return fns_1(x)
    if i == 2:
        return fns_2(x)

Note that, while it’s not evident from the above printed source code, each variable fns_X is assigned to the value of fns[X] at the time when the decoration occurs:

In [4]: call(0, math.pi)
Out[4]: 1.2246467991473532e-16  # AKA, sin(pi) = 0

In [5]: call(1, math.pi)
Out[5]: -1.0  # AKA, cos(pi) = -1

Unroll

Complete documentation:

In [1]: p_or_m = [1, -1]

In [2]: @pragma.unroll
   ...: def f(x):
   ...:     for j in range(3):
   ...:         for sign in p_or_m:
   ...:             yield sign * (x + j)
   ...:

In [3]: f??
Signature: f(x)
Source:
def f(x):
    yield 1 * (x + 0)
    yield -1 * (x + 0)
    yield 1 * (x + 1)
    yield -1 * (x + 1)
    yield 1 * (x + 2)
    yield -1 * (x + 2)

Inline

Complete documentation:

In [1]: def sqr(x):
   ...:     return x ** 2
   ...:

In [2]: @pragma.inline(sqr)
   ...: def sqr_sum(a, b):
   ...:     return sqr(a) + sqr(b)
   ...:

In [3]: sqr_sum??
Signature: sqr_sum(a, b)
Source:
def sqr_sum(a, b):
    _sqr_0 = dict(x=a)  # Prepare for 'sqr(a)'
    for ____ in [None]:  # Wrap function in block
        _sqr_0['return'] = _sqr_0['x'] ** 2  # Compute returned value
        break  # 'return'
    _sqr_return_0 = _sqr_0.get('return', None)  # Extract the returned value
    del _sqr_0  # Delete the arguments dictionary, the function call is finished
    _sqr_0 = dict(x=b)  # Do the same thing for 'sqr(b)'
    for ____ in [None]:
        _sqr_0['return'] = _sqr_0['x'] ** 2
        break
    _sqr_return_1 = _sqr_0.get('return', None)
    del _sqr_0
    return _sqr_return_0 + _sqr_return_1  # Substitute the returned values for the function calls

Indices and tables