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

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