- 8 DB models (services, incidents, monitors, subscribers, etc.) - Full CRUD API for services, incidents, monitors - Public status page with live data - Incident detail page with timeline - API key authentication - Uptime monitoring scheduler - 13 tests passing - TECHNICAL_DESIGN.md with full spec
310 lines
13 KiB
Python
310 lines
13 KiB
Python
"""Generate classes representing function environments (+ related operations).
|
|
|
|
If we have a nested function that has non-local (free) variables, access to the
|
|
non-locals is via an instance of an environment class. Example:
|
|
|
|
def f() -> int:
|
|
x = 0 # Make 'x' an attribute of an environment class instance
|
|
|
|
def g() -> int:
|
|
# We have access to the environment class instance to
|
|
# allow accessing 'x'
|
|
return x + 2
|
|
|
|
x = x + 1 # Modify the attribute
|
|
return g()
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from mypy.nodes import Argument, FuncDef, SymbolNode, Var
|
|
from mypyc.common import (
|
|
BITMAP_BITS,
|
|
ENV_ATTR_NAME,
|
|
GENERATOR_ATTRIBUTE_PREFIX,
|
|
SELF_NAME,
|
|
bitmap_name,
|
|
)
|
|
from mypyc.ir.class_ir import ClassIR
|
|
from mypyc.ir.ops import Call, GetAttr, SetAttr, Value
|
|
from mypyc.ir.rtypes import RInstance, bitmap_rprimitive, object_rprimitive
|
|
from mypyc.irbuild.builder import IRBuilder, SymbolTarget
|
|
from mypyc.irbuild.context import FuncInfo, GeneratorClass, ImplicitClass
|
|
from mypyc.irbuild.targets import AssignmentTargetAttr
|
|
|
|
|
|
def setup_env_class(builder: IRBuilder) -> ClassIR:
|
|
"""Generate a class representing a function environment.
|
|
|
|
Note that the variables in the function environment are not
|
|
actually populated here. This is because when the environment
|
|
class is generated, the function environment has not yet been
|
|
visited. This behavior is allowed so that when the compiler visits
|
|
nested functions, it can use the returned ClassIR instance to
|
|
figure out free variables it needs to access. The remaining
|
|
attributes of the environment class are populated when the
|
|
environment registers are loaded.
|
|
|
|
Return a ClassIR representing an environment for a function
|
|
containing a nested function.
|
|
"""
|
|
env_class = ClassIR(
|
|
f"{builder.fn_info.namespaced_name()}_env",
|
|
builder.module_name,
|
|
is_generated=True,
|
|
is_final_class=True,
|
|
)
|
|
env_class.reuse_freed_instance = True
|
|
env_class.attributes[SELF_NAME] = RInstance(env_class)
|
|
if builder.fn_info.is_nested and builder.fn_infos[-2]._env_class is not None:
|
|
# If the function is nested, its environment class must contain an environment
|
|
# attribute pointing to its encapsulating functions' environment class.
|
|
env_class.attributes[ENV_ATTR_NAME] = RInstance(builder.fn_infos[-2].env_class)
|
|
env_class.mro = [env_class]
|
|
builder.fn_info.env_class = env_class
|
|
builder.classes.append(env_class)
|
|
return env_class
|
|
|
|
|
|
def finalize_env_class(builder: IRBuilder, prefix: str = "") -> None:
|
|
"""Generate, instantiate, and set up the environment of an environment class."""
|
|
if not builder.fn_info.can_merge_generator_and_env_classes():
|
|
instantiate_env_class(builder)
|
|
|
|
# Iterate through the function arguments and replace local definitions (using registers)
|
|
# that were previously added to the environment with references to the function's
|
|
# environment class. Comprehension scopes have no arguments to add.
|
|
if not builder.fn_info.is_comprehension_scope:
|
|
if builder.fn_info.is_nested:
|
|
add_args_to_env(
|
|
builder, local=False, base=builder.fn_info.callable_class, prefix=prefix
|
|
)
|
|
else:
|
|
add_args_to_env(builder, local=False, base=builder.fn_info, prefix=prefix)
|
|
|
|
|
|
def instantiate_env_class(builder: IRBuilder) -> Value:
|
|
"""Assign an environment class to a register named after the given function definition."""
|
|
curr_env_reg = builder.add(
|
|
Call(builder.fn_info.env_class.ctor, [], builder.fn_info.fitem.line)
|
|
)
|
|
|
|
if builder.fn_info.is_nested and not builder.fn_info.is_comprehension_scope:
|
|
builder.fn_info.callable_class._curr_env_reg = curr_env_reg
|
|
builder.add(
|
|
SetAttr(
|
|
curr_env_reg,
|
|
ENV_ATTR_NAME,
|
|
builder.fn_info.callable_class.prev_env_reg,
|
|
builder.fn_info.fitem.line,
|
|
)
|
|
)
|
|
else:
|
|
# Top-level functions and comprehension scopes store env reg directly.
|
|
builder.fn_info._curr_env_reg = curr_env_reg
|
|
# Comprehension scopes link to parent env if it exists.
|
|
if (
|
|
builder.fn_info.is_nested
|
|
and builder.fn_infos[-2]._env_class is not None
|
|
and builder.fn_infos[-2]._curr_env_reg is not None
|
|
):
|
|
builder.add(
|
|
SetAttr(
|
|
curr_env_reg,
|
|
ENV_ATTR_NAME,
|
|
builder.fn_infos[-2].curr_env_reg,
|
|
builder.fn_info.fitem.line,
|
|
)
|
|
)
|
|
|
|
return curr_env_reg
|
|
|
|
|
|
def load_env_registers(builder: IRBuilder, prefix: str = "") -> None:
|
|
"""Load the registers for the current FuncItem being visited.
|
|
|
|
Adds the arguments of the FuncItem to the environment. If the
|
|
FuncItem is nested inside of another function, then this also
|
|
loads all of the outer environments of the FuncItem into registers
|
|
so that they can be used when accessing free variables.
|
|
"""
|
|
add_args_to_env(builder, local=True, prefix=prefix)
|
|
|
|
fn_info = builder.fn_info
|
|
fitem = fn_info.fitem
|
|
if fn_info.is_nested and builder.fn_infos[-2]._env_class is not None:
|
|
load_outer_envs(builder, fn_info.callable_class)
|
|
# If this is a FuncDef, then make sure to load the FuncDef into its own environment
|
|
# class so that the function can be called recursively.
|
|
if isinstance(fitem, FuncDef) and fn_info.add_nested_funcs_to_env:
|
|
setup_func_for_recursive_call(builder, fitem, fn_info.callable_class, prefix=prefix)
|
|
|
|
|
|
def load_outer_env(
|
|
builder: IRBuilder, base: Value, outer_env: dict[SymbolNode, SymbolTarget]
|
|
) -> Value:
|
|
"""Load the environment class for a given base into a register.
|
|
|
|
Additionally, iterates through all of the SymbolNode and
|
|
AssignmentTarget instances of the environment at the given index's
|
|
symtable, and adds those instances to the environment of the
|
|
current environment. This is done so that the current environment
|
|
can access outer environment variables without having to reload
|
|
all of the environment registers.
|
|
|
|
Returns the register where the environment class was loaded.
|
|
"""
|
|
env = builder.add(GetAttr(base, ENV_ATTR_NAME, builder.fn_info.fitem.line))
|
|
assert isinstance(env.type, RInstance), f"{env} must be of type RInstance"
|
|
|
|
for symbol, target in outer_env.items():
|
|
attr_name = symbol.name
|
|
if isinstance(target, AssignmentTargetAttr):
|
|
attr_name = target.attr
|
|
env.type.class_ir.attributes[attr_name] = target.type
|
|
symbol_target = AssignmentTargetAttr(env, attr_name)
|
|
builder.add_target(symbol, symbol_target)
|
|
|
|
return env
|
|
|
|
|
|
def load_outer_envs(builder: IRBuilder, base: ImplicitClass) -> None:
|
|
index = len(builder.builders) - 2
|
|
|
|
# Load the first outer environment. This one is special because it gets saved in the
|
|
# FuncInfo instance's prev_env_reg field.
|
|
has_outer = index > 1 or (index == 1 and builder.fn_infos[1].contains_nested)
|
|
if has_outer and builder.fn_infos[index]._env_class is not None:
|
|
# outer_env = builder.fn_infos[index].environment
|
|
outer_env = builder.symtables[index]
|
|
if isinstance(base, GeneratorClass):
|
|
base.prev_env_reg = load_outer_env(builder, base.curr_env_reg, outer_env)
|
|
else:
|
|
base.prev_env_reg = load_outer_env(builder, base.self_reg, outer_env)
|
|
env_reg = base.prev_env_reg
|
|
index -= 1
|
|
|
|
# Load the remaining outer environments into registers.
|
|
while index > 1:
|
|
if builder.fn_infos[index]._env_class is None:
|
|
break
|
|
# outer_env = builder.fn_infos[index].environment
|
|
outer_env = builder.symtables[index]
|
|
env_reg = load_outer_env(builder, env_reg, outer_env)
|
|
index -= 1
|
|
|
|
|
|
def num_bitmap_args(builder: IRBuilder, args: list[Argument]) -> int:
|
|
n = 0
|
|
for arg in args:
|
|
t = builder.type_to_rtype(arg.variable.type)
|
|
if t.error_overlap and arg.kind.is_optional():
|
|
n += 1
|
|
return (n + (BITMAP_BITS - 1)) // BITMAP_BITS
|
|
|
|
|
|
def add_args_to_env(
|
|
builder: IRBuilder,
|
|
local: bool = True,
|
|
base: FuncInfo | ImplicitClass | None = None,
|
|
reassign: bool = True,
|
|
prefix: str = "",
|
|
) -> None:
|
|
fn_info = builder.fn_info
|
|
args = fn_info.fitem.arguments
|
|
nb = num_bitmap_args(builder, args)
|
|
if local:
|
|
for arg in args:
|
|
rtype = builder.type_to_rtype(arg.variable.type)
|
|
builder.add_local_reg(arg.variable, rtype, is_arg=True)
|
|
for i in reversed(range(nb)):
|
|
builder.add_local_reg(Var(bitmap_name(i)), bitmap_rprimitive, is_arg=True)
|
|
else:
|
|
for arg in args:
|
|
if (
|
|
is_free_variable(builder, arg.variable)
|
|
or fn_info.is_generator
|
|
or fn_info.is_coroutine
|
|
):
|
|
rtype = builder.type_to_rtype(arg.variable.type)
|
|
assert base is not None, "base cannot be None for adding nonlocal args"
|
|
builder.add_var_to_env_class(
|
|
arg.variable, rtype, base, reassign=reassign, prefix=prefix
|
|
)
|
|
|
|
|
|
def add_vars_to_env(builder: IRBuilder, prefix: str = "") -> None:
|
|
"""Add relevant local variables and nested functions to the environment class.
|
|
|
|
Add all variables and functions that are declared/defined within current
|
|
function and are referenced in functions nested within this one to this
|
|
function's environment class so the nested functions can reference
|
|
them even if they are declared after the nested function's definition.
|
|
Note that this is done before visiting the body of the function.
|
|
"""
|
|
env_for_func: FuncInfo | ImplicitClass = builder.fn_info
|
|
if builder.fn_info.is_generator:
|
|
env_for_func = builder.fn_info.generator_class
|
|
elif (
|
|
builder.fn_info.is_nested or builder.fn_info.in_non_ext
|
|
) and not builder.fn_info.is_comprehension_scope:
|
|
env_for_func = builder.fn_info.callable_class
|
|
|
|
if builder.fn_info.fitem in builder.free_variables:
|
|
# Sort the variables to keep things deterministic
|
|
for var in sorted(builder.free_variables[builder.fn_info.fitem], key=lambda x: x.name):
|
|
if isinstance(var, Var):
|
|
rtype = builder.type_to_rtype(var.type)
|
|
builder.add_var_to_env_class(
|
|
var, rtype, env_for_func, reassign=False, prefix=prefix
|
|
)
|
|
|
|
if builder.fn_info.fitem in builder.encapsulating_funcs:
|
|
for nested_fn in builder.encapsulating_funcs[builder.fn_info.fitem]:
|
|
if isinstance(nested_fn, FuncDef):
|
|
# The return type is 'object' instead of an RInstance of the
|
|
# callable class because differently defined functions with
|
|
# the same name and signature across conditional blocks
|
|
# will generate different callable classes, so the callable
|
|
# class that gets instantiated must be generic.
|
|
if nested_fn.is_generator or nested_fn.is_coroutine:
|
|
prefix = GENERATOR_ATTRIBUTE_PREFIX
|
|
builder.add_var_to_env_class(
|
|
nested_fn, object_rprimitive, env_for_func, reassign=False, prefix=prefix
|
|
)
|
|
|
|
|
|
def setup_func_for_recursive_call(
|
|
builder: IRBuilder, fdef: FuncDef, base: ImplicitClass, prefix: str = ""
|
|
) -> None:
|
|
"""Enable calling a nested function (with a callable class) recursively.
|
|
|
|
Adds the instance of the callable class representing the given
|
|
FuncDef to a register in the environment so that the function can
|
|
be called recursively. Note that this needs to be done only for
|
|
nested functions.
|
|
"""
|
|
# First, set the attribute of the environment class so that GetAttr can be called on it.
|
|
prev_env = builder.fn_infos[-2].env_class
|
|
attr_name = prefix + fdef.name
|
|
prev_env.attributes[attr_name] = builder.type_to_rtype(fdef.type)
|
|
line = fdef.line
|
|
|
|
if isinstance(base, GeneratorClass):
|
|
# If we are dealing with a generator class, then we need to first get the register
|
|
# holding the current environment class, and load the previous environment class from
|
|
# there.
|
|
prev_env_reg = builder.add(GetAttr(base.curr_env_reg, ENV_ATTR_NAME, line))
|
|
else:
|
|
prev_env_reg = base.prev_env_reg
|
|
|
|
# Obtain the instance of the callable class representing the FuncDef, and add it to the
|
|
# current environment.
|
|
val = builder.add(GetAttr(prev_env_reg, attr_name, line))
|
|
target = builder.add_local_reg(fdef, object_rprimitive)
|
|
builder.assign(target, val, line)
|
|
|
|
|
|
def is_free_variable(builder: IRBuilder, symbol: SymbolNode) -> bool:
|
|
fitem = builder.fn_info.fitem
|
|
return fitem in builder.free_variables and symbol in builder.free_variables[fitem]
|