Commit 0a0cdfd7 authored by Tom Niget's avatar Tom Niget

Scopes work? I think – handles nested decls in functions

parent 4aa55569
......@@ -7,11 +7,16 @@ test = (2 + 3) * 4
glob = 5
def g():
a = 8
if True:
b = 9
if True:
c = 10
if True:
x = 5
print(x)
d = a + b + c
if True:
e = d + 1
print(e)
def f(x):
return x + 1
......
......@@ -146,8 +146,7 @@ class PrecedenceContext:
self.op = op
def __enter__(self):
if self.visitor.precedence[-1:] != [self.op]:
self.visitor.precedence.append(self.op)
self.visitor.precedence.append(self.op)
def __exit__(self, exc_type, exc_val, exc_tb):
self.visitor.precedence.pop()
......@@ -290,18 +289,82 @@ class ExpressionVisitor(NodeVisitor):
yield " : "
yield from self.visit(node.orelse)
@dataclass
class VarDecl:
kind: VarKind
val: Optional[str]
@dataclass
class Scope:
parent: Optional["Scope"] = None
vars: Dict[str, VarKind] = field(default_factory=dict)
is_function: bool = False
vars: Dict[str, VarDecl] = field(default_factory=dict)
def is_global(self):
def is_global(self) -> bool:
"""
Determines whether this scope is the global scope. The global scope is the only scope to have no parent.
"""
return self.parent is None
def exists(self, name: str) -> bool:
"""
Determines whether a variable exists in the current scope or any parent scope.
"""
return name in self.vars or (self.parent is not None and self.parent.exists(name))
def exists_local(self, name: str) -> bool:
"""
Determines whether a variable exists in the current function or global scope.
The check does not cross function boundaries; i.e. global variables are not taken into account from inside
functions.
"""
return name in self.vars or (not self.is_function and self.parent is not None and self.parent.exists_local(name))
def child(self) -> "Scope":
"""
Creates a child scope with a new variable dictionary.
This is used for first-level elements of a function.
"""
return Scope(self, False, {})
def child_share(self) -> "Scope":
"""
Creates a child scope sharing the variable dictionary with the parent scope.
This is used for Python blocks, which share the variable scope with their parent block.
"""
return Scope(self, False, self.vars)
def function(self, **kwargs) -> "Scope":
"""
Creates a function scope.
"""
return Scope(self, True, **kwargs)
def is_root(self) -> Optional[Dict[str, VarDecl]]:
"""
Determines whether this scope is a root scope.
A root scope is either the global scope, or the first inner scope of a function.
Variable declarations in the generated code only ever appear in root scopes.
:return: `None` if this scope is not a root scope, otherwise the variable dictionary of the root scope.
"""
if self.parent is None:
return self.vars
if self.parent.is_function:
return self.parent.vars
return None
def declare(self, name: str, val: Optional[str] = None) -> Optional[str]:
if self.exists_local(name):
# If the variable already exists in the current function or global scope, we don't need to declare it again.
# This is simply an assignment.
return None
vdict, prefix = self.vars, ""
if (root_vars := self.is_root()) is not None:
vdict, prefix = root_vars, "auto " # Root scope declarations can use `auto`.
vdict[name] = VarDecl(VarKind.LOCAL, val)
return prefix
# noinspection PyPep8Naming
class BlockVisitor(NodeVisitor):
......@@ -340,44 +403,94 @@ class BlockVisitor(NodeVisitor):
yield f"auto {node.name}"
yield args
yield "{"
inner = BlockVisitor(Scope(self._scope, vars={arg.arg: VarKind.LOCAL for arg in node.args.args}))
inner = BlockVisitor(self._scope.function())
for child in node.body:
yield from inner.visit(child)
# Python uses module- and function- level scoping. Blocks, like conditionals and loops, do not form scopes
# on their own. Variables are still accessible in the remainder of the parent function or in the global
# scope if outside a function.
# This is different from C++, where scope is tied to any code block. To emulate this behavior, we need to
# declare all variables in the first inner scope of a function.
# For example,
# ```py
# def f():
# if True:
# x = 1
# print(x)
# ```
# is translated to
# ```cpp
# auto f() {
# decltype(1) x;
# if (true) {
# x = 1;
# }
# print(x);
# }
# ```
# `decltype` allows for proper typing (`auto` can't be used for variables with values later assigned, since
# this would require real type inference, akin to what Rust does).
# This is only done, though, for *nested* blocks of a function. Root-level variables are declared with
# `auto`:
# ```py
# x = 1
# def f():
# y = 2
# ```
# is translated to
# ```cpp
# auto x = 1;
# auto f() {
# auto y = 2;
# }
# ```
child_visitor = BlockVisitor(inner._scope.child())
# We need to do this in two-passes. This unfortunately breaks our nice generator state-machine architecture.
# Fair enough.
[*child_code] = child_visitor.visit(child)
# Hoist inner variables to the root scope.
for var, decl in child_visitor._scope.vars.items():
if decl.kind == VarKind.LOCAL: # Nested declarations become `decltype` declarations.
yield f"decltype({decl.val}) {var};"
elif decl.kind in (VarKind.GLOBAL, VarKind.NONLOCAL): # `global` and `nonlocal` just get hoisted as-is.
inner._scope.vars[var] = decl
yield from child_code # Yeet back the child node code.
yield "}"
def visit_Global(self, node: ast.Global) -> Iterable[str]:
for name in map(self.fix_name, node.names):
self._scope.vars[name] = VarKind.GLOBAL
self._scope.vars[name] = VarDecl(VarKind.GLOBAL, None)
yield ""
def visit_Nonlocal(self, node: ast.Nonlocal) -> Iterable[str]:
for name in map(self.fix_name, node.names):
self._scope.vars[name] = VarKind.NONLOCAL
self._scope.vars[name] = VarDecl(VarKind.NONLOCAL, None)
yield ""
def visit_If(self, node: ast.If) -> Iterable[str]:
if not node.orelse and compare_ast(node.test, ast.parse('__name__ == "__main__"', mode="eval").body):
yield "int main() {"
for child in node.body:
yield from self.visit(child)
yield "}"
# Special case handling for Python's interesting way of defining an entry point.
# I mean, it's not *that* bad, it's just an attempt at retrofitting an "entry point" logic in a scripting
# language that, by essence, uses "the start of the file" as the implicit entry point, since files are
# read and executed line-by-line, contrary to usual structured languages that mark a distinction between
# declarations (functions, classes, modules, ...) and code.
# Also, for nitpickers, the C++ standard explicitly allows for omitting a `return` statement in the `main`.
# 0 is returned by default.
yield "int main()"
yield from self.emit_block(node.body)
return
yield "if ("
yield from ExpressionVisitor().visit(node.test)
yield ") {"
for child in node.body:
yield from self.visit(child)
yield "}"
yield ")"
yield from self.emit_block(node.body)
if node.orelse:
yield "else "
if isinstance(node.orelse, ast.If):
yield from self.visit(node.orelse)
else:
yield "{"
for child in node.orelse:
yield from self.visit(child)
yield "}"
yield from self.emit_block(node.orelse)
def visit_Return(self, node: ast.Return) -> Iterable[str]:
yield "return "
......@@ -388,21 +501,19 @@ class BlockVisitor(NodeVisitor):
def visit_While(self, node: ast.While) -> Iterable[str]:
yield "while ("
yield from ExpressionVisitor().visit(node.test)
yield ") {"
for child in node.body:
yield from self.visit(child)
yield "}"
yield ")"
yield from self.emit_block(node.body)
if node.orelse:
raise NotImplementedError(node, "orelse")
def visit_lvalue(self, lvalue: ast.expr) -> Iterable[str]:
def visit_lvalue(self, lvalue: ast.expr, val: Optional[ast.AST] = None) -> Iterable[str]:
if isinstance(lvalue, ast.Tuple):
yield f"std::tie({', '.join(flatmap(ExpressionVisitor().visit, lvalue.elts))})"
elif isinstance(lvalue, ast.Name):
name = self.fix_name(lvalue.id)
if name not in self._scope.vars:
self._scope.vars[name] = name
yield "auto "
#if name not in self._scope.vars:
if not self._scope.exists_local(name):
yield self._scope.declare(name, " ".join(ExpressionVisitor().visit(val)) if val else None)
yield name
elif isinstance(lvalue, ast.Subscript):
yield from ExpressionVisitor().visit(lvalue)
......@@ -412,7 +523,7 @@ class BlockVisitor(NodeVisitor):
def visit_Assign(self, node: ast.Assign) -> Iterable[str]:
if len(node.targets) != 1:
raise NotImplementedError(node)
yield from self.visit_lvalue(node.targets[0])
yield from self.visit_lvalue(node.targets[0], node.value)
yield " = "
yield from ExpressionVisitor().visit(node.value)
yield ";"
......@@ -420,7 +531,7 @@ class BlockVisitor(NodeVisitor):
def visit_AnnAssign(self, node: ast.AnnAssign) -> Iterable[str]:
if node.value is None:
raise NotImplementedError(node, "empty value")
yield from self.visit_lvalue(node.target)
yield from self.visit_lvalue(node.target, node.value)
yield " = "
yield from ExpressionVisitor().visit(node.value)
yield ";"
......@@ -436,9 +547,20 @@ class BlockVisitor(NodeVisitor):
raise NotImplementedError(node)
yield f"for (auto {node.target.id} : "
yield from ExpressionVisitor().visit(node.iter)
yield ") {"
for child in node.body:
yield from self.visit(child)
yield "}"
yield ")"
yield from self.emit_block(node.body)
if node.orelse:
raise NotImplementedError(node, "orelse")
def block(self) -> "BlockVisitor":
# See the comments in visit_FunctionDef.
# A Python code block does not introduce a new scope, so we create a new `Scope` object that shares the same
# variables as the parent scope.
return BlockVisitor(self._scope.child_share())
def emit_block(self, items: List[ast.stmt]) -> Iterable[str]:
yield "{"
block = self.block()
for child in items:
yield from block.visit(child)
yield "}"
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment