Show HN: Python decorator that enables arbitrarily-deep tail/non-tail recursion

Show HN: Python decorator that enables arbitrarily-deep tail/non-tail recursion

Fiber implements an proof-of-concept Python decorator that rewrites a plan
so that it could per chance most likely perchance presumably even be paused and resumed (by shifting stack variables to a heap frame
and at the side of if statements to simulate jumps/gotos to specific strains of code).

Then, the employ of a trampoline plan that simulates the name stack on the heap, we
can name functions that recurse arbitrarily deeply with out stack overflowing
(assuming we net now not scuttle out of heap memory).

Please net now not employ this in manufacturing.


The most sensible likely plan it works

A transient refresher on the name stack: assuredly, when some plan A calls
yet any other plan B, A is “paused” whereas B runs to completion. Then, as soon as B
finishes, A is resumed.

In repeat to pass the name stack to the heap, we like now to transform plan A
to (1) store all variables on the heap, and (2) be in a field to resume execution
at specific strains of code for the period of the plan.

The main step is discreet: we rewrite all native hundreds and shops to as an change load
and store in a frame dictionary that’s handed into the plan. The 2nd is
extra complicated: because Python would not give a take to goto statements, we like now to
insert if statements to skip the code prefix that we net now not wish to create.

There are a form of “special

that can perchance now not be jumped into. These we should address by rewriting them precise into a develop
that we net address.

To illustrate, if we recursively name a plan interior a for loop, we would treasure
in tell to resume execution on the identical iteration. On the opposite hand, when Python
executes a for loop on an non-iterator iterable this could occasionally maintain a new iterator
every time. To address this case, we rewrite for loops into the an identical whereas
loop. Equally, we should rewrite boolean expressions that fast circuit (and,
or) into the an identical if statements.

Lastly, we should replace all recursive calls and widespread returns by as an change
returning an instruction to a trampoline to name the newborn plan or return
the worth to the mother or father plan, respectively.

To recap, right here are the AST passes we for the time being put in power:

  1. Rewrite special kinds:
    • for_to_while: Transforms for loops into the an identical whereas loops.
    • promote_while_cond: Rewrites the whereas conditional to employ a non everlasting
      variable that’s up to this point every loop iteration so that we are able to management when
      it’s miles evaluated (e.g. if the loop situation involves a recursive name).
    • bool_exps_to_if: Converts and and or expressions into the
      an identical if statements.
  2. promote_to_temporary: Assigns the outcomes of recursive calls into
    non everlasting variables. This is excessive when we maintain extra than one recursive calls
    in the identical assertion (e.g. fib(n-1) + fib(n-2)): we like now to resume
    execution in the center of the expression.
  3. remove_trivial_temporaries: Removes temporaries that are assigned to advantageous
    as soon as and are at as soon as assigned to a pair of alternative variable, replacing subsequent
    usages with that other variable. This helps us detect tail calls.
  4. insert_jumps: Marks the assertion after yield capabilities (for the time being recursive
    calls and widespread returns) with a pocket e-book computer index, and inserts if statements so
    that re-execution of the plan will resume at that program counter.
  5. lift_locals_to_frame: Replaces hundreds and shops of native variables to
    hundreds and shops in the frame object.
  6. add_trampoline_returns: Replaces areas the place we should yield (recursive
    calls and widespread returns) with returns to the trampoline plan.
  7. fix_fn_def: Rewrites the plan defintion to rob a frame parameter.

Seek the examples directory for functions and the outcomes after
every AST drag. Additionally, leer src/ for
some take a look at cases.


A easy tail-recursive plan that computes the sum of an array takes about
10-11 seconds to compute with Fiber. 1000 iterations of the an identical for loop
takes 7-8 seconds to compute. So we’re slower by roughly a a part of 1000.

lst = checklist(differ(1, 100001))

# fiber
def sum(lst, acc):
    if now not lst:
        return acc
    return sum(lst[1:], acc + lst[0])

# for loop
total = 0
for i in lst:
    total += i

print(total, trampoline.scuttle(sum, [lst, 0]))  # 5000050000, 5000050000

We could per chance reinforce the efficiency of the code by eliminating redundant if
tests in the generated code. Additionally, as we statically know the stack variables,
we are able to employ an array for the stack frame and integer indexes (in decision to a
dictionary and string hashes + lookups). This could increasingly perchance like to enhance the efficiency
seriously, nonetheless there’ll restful potentially be a enormous amount of overhead.

One more efficiency enchancment is to inline the stack array: in decision to
storing a checklist of frames in the trampoline, we would variables at as soon as in the
stack. All over again, we are able to compute the frame dimension statically. Per some assessments in
a handwritten JavaScript implementation, this has the aptitude to tempo up the
code by roughly a a part of 2-3, on the worth of a extra complicated implementation.


  • The transformation works on the AST stage, so we net now not give a take to other
    decorators (as an illustration, we are able to now not employ
    in the above Fibonacci instance).

  • The plan can advantageous access variables that are handed in the locals=
    argument. Attributable to this, to resolve recursive plan calls,
    we assist a world mapping of all fiber functions by name. This design that
    fibers have to love trot names.

  • We net now not give a take to a pair of special kinds (ternaries, comprehensions). These can
    with out complications be added as a rewrite transformation.

  • We net now not give a take to exceptions. This could per chance require us to assist song of exception
    handlers in the trampoline and insert returns to the trampoline to register
    and deregister handlers.

  • We net now not give a take to mills. So that you just can add give a take to, we would deserve to alter the
    trampoline to accept yet any other operation form (yield) that sends a worth to the
    plan that known as next(). Additionally, the trampoline would deserve to present a take to
    extra than one name stacks.

That you just’ll want to per chance presumably presumably additionally take into consideration improvements

  • Toughen take a look at protection on a pair of of the AST transformations.
    • remove_trivial_temporaries could per chance presumably like a pc virus if the variable that it’s miles
      changed with is reassigned to yet any other worth.
  • Give a take to out of the ordinary kinds (comprehensions, mills).
  • Give a take to exceptions.
  • Give a take to recursive calls that net now not study the return worth.


Why did not you make employ of Python mills?

It be much less engrossing as the transformations are less complicated. Here, we’re
effectively implementing mills in userspace (i.e. now not desiring VM give a take to);
leer the resolution to the subsequent ask for why this is righteous.

Additionally, other folks like mature mills to net this; leer one most contemporary generator

Why did you write this?

  • A+ mission for CS 61A at

    All the plan through the direction, we created a Scheme interpreter. The extra credit
    ask we to interchange tail calls in Python with a return to a trampoline,
    with the plan that tail name optimization in Python would let us review
    tail calls to arbitrary depth in Scheme, in fixed condominium.

    The take a look at cases for the ask checked whether or now not decoding tail-name
    recursive functions in Scheme brought on a Python stack overflow. The employ of this
    Fiber implementation, (1) with out tail name optimization in our trampoline,
    we would restful be in a field to drag the take a look at cases (we upright wouldn’t employ fixed
    condominium) and (2) we are able to now review any Scheme expression to arbitrary depth,
    even though they’re now not in tail develop.

  • The React framework has an a pc virus originate which explores a compiler transform to
    rewrite JavaScript mills to a recount machine so that recursive operations
    (render, reconcilation) could also be written extra with out complications. This is excessive because
    some JavaScript engines restful net now not give a take to mills.

    This mission in total implements a tough model of that compiler transform
    as a proof of concept, upright in Python.



Be half of the pack! Be half of 8000+ others registered users, and net chat, maintain teams, post updates and maintain website online visitors across the realm!

Ava Chan

Ava Chan

I'm a researcher at Utokyo :) and a big fan of Ava Max