Skip to content

Compiling Nested Functions

Jukka Lehtosalo edited this page Oct 24, 2022 · 2 revisions

Nested functions can access variables defined in an outer function:

def f(x: int) -> None:
    def g() -> None:
        print(x)  # Refer to x in the outer function
    g()

To support this, mypyc creates a hidden environment class, and instances of this class hold locals that are accessed in nested functions. The nested function is also represented as an instance of a generated class which has a reference to the environment. The example above behaves like this when compiled, roughly:

class f_env:  # Generated class, implementation detail
    x: int
    g: g_f_obj

class g_f_obj:  # Generated class, implementation detail
    __mypyc_env__: f_env

    def __call__(self) -> None:
        print(self.__mypyc_env__.x)

def f(x: int) -> None:
    _env = f_env()
    _env.x = x
    g = g_f_obj()
    g.__mypyc_env__ = _env
    _env.g = g
    g()

The environment class _env also holds a reference to the nested function g to allow recursive calls in g, and mutually recursive calls if there are multiple nested functions. The nested function has no direct access to the locals of the outer function, other than those accessible via the environment. In this case there are no recursive calls so we could drop the g attribute. It's sometimes needed and we generate it always, for simplicity.

The environment and the nested function object are separate instances so that locals from an outer function can be shared between multiple nested functions. If there was only a single nested function, mypyc could merge the environment and the function object (but this is not implemented).

Relevant Code

  • mypyc/irbuild/envclass.py (environment classes)
  • mypyc/irbuild/callable_class (nested function objects)