fix(resolver): properly check if variable is defined
This commit is contained in:
@@ -107,7 +107,7 @@ class PythonTyper(
|
|||||||
tree: ast.Module = ast.parse(source, filename=path or "<unknown>")
|
tree: ast.Module = ast.parse(source, filename=path or "<unknown>")
|
||||||
parser = PythonParser()
|
parser = PythonParser()
|
||||||
stmts: list[p.Stmt] = parser.parse_module(tree)
|
stmts: list[p.Stmt] = parser.parse_module(tree)
|
||||||
resolver = Resolver()
|
resolver = Resolver(reporter)
|
||||||
resolver.resolve(*stmts)
|
resolver.resolve(*stmts)
|
||||||
|
|
||||||
self.env = self.global_env
|
self.env = self.global_env
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import midas.ast.python as p
|
import midas.ast.python as p
|
||||||
|
from midas.ast.location import Location
|
||||||
|
from midas.checker.reporter import FileReporter
|
||||||
|
|
||||||
|
|
||||||
class ResolverError(Exception): ...
|
class ResolverError(Exception): ...
|
||||||
@@ -11,9 +13,10 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
|||||||
scope is referred to when a variable is referenced
|
scope is referred to when a variable is referenced
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, reporter: FileReporter):
|
||||||
self.locals: dict[p.Expr, int] = {}
|
self.locals: dict[p.Expr, int] = {}
|
||||||
self.scopes: list[dict[str, bool]] = [{}]
|
self.scopes: list[dict[str, bool]] = [{}]
|
||||||
|
self.reporter: FileReporter = reporter
|
||||||
|
|
||||||
def resolve(self, *objects: p.Stmt | p.Expr) -> None:
|
def resolve(self, *objects: p.Stmt | p.Expr) -> None:
|
||||||
"""Resolve the given statements or expressions"""
|
"""Resolve the given statements or expressions"""
|
||||||
@@ -25,29 +28,28 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
|||||||
"""Begin a new scope inside the current one"""
|
"""Begin a new scope inside the current one"""
|
||||||
self.scopes.append({})
|
self.scopes.append({})
|
||||||
|
|
||||||
def end_scope(self):
|
def end_scope(self) -> dict[str, bool]:
|
||||||
"""Close the current scope"""
|
"""Close and return the current scope"""
|
||||||
self.scopes.pop()
|
return self.scopes.pop()
|
||||||
|
|
||||||
def declare(self, name: str) -> None:
|
def declare(self, location: Location, name: str) -> None:
|
||||||
"""Declare a variable in the current scope
|
"""Declare a variable in the current scope
|
||||||
|
|
||||||
This method must be called *before* evaluating the variable initializer
|
This method must be called *before* evaluating the variable initializer
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name (str): the name of the variable
|
name (str): the name of the variable
|
||||||
|
|
||||||
Raises:
|
|
||||||
ResolverError: if the variable has already been declared in the current scope
|
|
||||||
"""
|
"""
|
||||||
if len(self.scopes) == 0:
|
if len(self.scopes) == 0:
|
||||||
return
|
return
|
||||||
scope: dict[str, bool] = self.scopes[-1]
|
scope: dict[str, bool] = self.scopes[-1]
|
||||||
if name in scope:
|
if name in scope:
|
||||||
raise ResolverError(
|
self.reporter.error(
|
||||||
f"A variable with the name {name} is already declared in this scope"
|
location,
|
||||||
|
f"A variable with the name '{name}' is already declared in this scope",
|
||||||
)
|
)
|
||||||
scope[name] = False
|
else:
|
||||||
|
scope[name] = False
|
||||||
|
|
||||||
def define(self, name: str) -> None:
|
def define(self, name: str) -> None:
|
||||||
"""Define a variable in the current scope
|
"""Define a variable in the current scope
|
||||||
@@ -77,7 +79,7 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
|||||||
self.locals[expr] = i
|
self.locals[expr] = i
|
||||||
return
|
return
|
||||||
|
|
||||||
def is_defined(self, name: str) -> bool:
|
def is_declared(self, name: str) -> bool:
|
||||||
"""Check whether the given variable is defined in any scope
|
"""Check whether the given variable is defined in any scope
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -106,7 +108,7 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
|||||||
self.resolve(param.default)
|
self.resolve(param.default)
|
||||||
|
|
||||||
for param in function.params.all:
|
for param in function.params.all:
|
||||||
self.declare(param.name)
|
self.declare(function.location, param.name)
|
||||||
self.define(param.name)
|
self.define(param.name)
|
||||||
self.resolve(*function.body)
|
self.resolve(*function.body)
|
||||||
self.end_scope()
|
self.end_scope()
|
||||||
@@ -116,14 +118,12 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
|||||||
|
|
||||||
def visit_function(self, stmt: p.Function) -> None:
|
def visit_function(self, stmt: p.Function) -> None:
|
||||||
# Declare before resolving body to allow recursion
|
# Declare before resolving body to allow recursion
|
||||||
self.declare(stmt.name)
|
self.declare(stmt.location, stmt.name)
|
||||||
self.define(stmt.name)
|
self.define(stmt.name)
|
||||||
self.resolve_function(stmt)
|
self.resolve_function(stmt)
|
||||||
|
|
||||||
def visit_type_assign(self, stmt: p.TypeAssign) -> None:
|
def visit_type_assign(self, stmt: p.TypeAssign) -> None:
|
||||||
self.declare(stmt.name)
|
self.declare(stmt.location, stmt.name)
|
||||||
# NOTE: resolve type here?
|
|
||||||
self.define(stmt.name)
|
|
||||||
|
|
||||||
def visit_assign_stmt(self, stmt: p.AssignStmt) -> None:
|
def visit_assign_stmt(self, stmt: p.AssignStmt) -> None:
|
||||||
self.resolve(stmt.value)
|
self.resolve(stmt.value)
|
||||||
@@ -133,9 +133,9 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
|||||||
def _visit_assign(self, target: p.Expr):
|
def _visit_assign(self, target: p.Expr):
|
||||||
match target:
|
match target:
|
||||||
case p.VariableExpr(name=name):
|
case p.VariableExpr(name=name):
|
||||||
if not self.is_defined(name):
|
if not self.is_declared(name):
|
||||||
self.declare(name)
|
self.declare(target.location, name)
|
||||||
self.define(name)
|
self.define(name)
|
||||||
target.accept(self)
|
target.accept(self)
|
||||||
|
|
||||||
case p.GetExpr():
|
case p.GetExpr():
|
||||||
@@ -145,7 +145,9 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
|||||||
target.accept(self)
|
target.accept(self)
|
||||||
|
|
||||||
case _:
|
case _:
|
||||||
raise Exception(f"Unsupported assignment to {target}")
|
self.reporter.error(
|
||||||
|
target.location, f"Unsupported assignment to {target}"
|
||||||
|
)
|
||||||
|
|
||||||
def visit_return_stmt(self, stmt: p.ReturnStmt) -> None:
|
def visit_return_stmt(self, stmt: p.ReturnStmt) -> None:
|
||||||
if stmt.value is not None:
|
if stmt.value is not None:
|
||||||
@@ -162,12 +164,17 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
|||||||
# Body
|
# Body
|
||||||
self.begin_scope()
|
self.begin_scope()
|
||||||
self.resolve(*stmt.body)
|
self.resolve(*stmt.body)
|
||||||
self.end_scope()
|
body: dict[str, bool] = self.end_scope()
|
||||||
|
|
||||||
# Else
|
# Else
|
||||||
self.begin_scope()
|
self.begin_scope()
|
||||||
self.resolve(*stmt.orelse)
|
self.resolve(*stmt.orelse)
|
||||||
self.end_scope()
|
else_: dict[str, bool] = self.end_scope()
|
||||||
|
|
||||||
|
# Define variables in this scope if it was defined in both body and else blocks
|
||||||
|
for name, is_defined in body.items():
|
||||||
|
if is_defined and else_.get(name, False):
|
||||||
|
self.define(name)
|
||||||
|
|
||||||
def visit_pass(self, stmt: p.Pass) -> None:
|
def visit_pass(self, stmt: p.Pass) -> None:
|
||||||
pass
|
pass
|
||||||
@@ -188,7 +195,7 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
|||||||
def _resolve_imports(self, imports: list[p.ImportAlias]) -> None:
|
def _resolve_imports(self, imports: list[p.ImportAlias]) -> None:
|
||||||
for import_ in imports:
|
for import_ in imports:
|
||||||
name: str = import_.imported_name
|
name: str = import_.imported_name
|
||||||
self.declare(name)
|
self.declare(import_.location, name)
|
||||||
self.define(name)
|
self.define(name)
|
||||||
|
|
||||||
def visit_raw_stmt(self, stmt: p.RawStmt) -> None:
|
def visit_raw_stmt(self, stmt: p.RawStmt) -> None:
|
||||||
@@ -220,8 +227,9 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
|||||||
|
|
||||||
def visit_variable_expr(self, expr: p.VariableExpr) -> None:
|
def visit_variable_expr(self, expr: p.VariableExpr) -> None:
|
||||||
if len(self.scopes) != 0 and self.scopes[-1].get(expr.name) is False:
|
if len(self.scopes) != 0 and self.scopes[-1].get(expr.name) is False:
|
||||||
raise ResolverError(
|
self.reporter.error(
|
||||||
f"Cannot use local variable '{expr.name}' in its own initializer"
|
expr.location,
|
||||||
|
f"Variable '{expr.name}' is declared but may not be defined",
|
||||||
) # aka. UnboundLocalError
|
) # aka. UnboundLocalError
|
||||||
self.resolve_local(expr, expr.name)
|
self.resolve_local(expr, expr.name)
|
||||||
|
|
||||||
|
|||||||
@@ -11,16 +11,16 @@ from _ import (
|
|||||||
Unused,
|
Unused,
|
||||||
)
|
)
|
||||||
|
|
||||||
unused: Unused
|
unused: Unused = object()
|
||||||
covariant: Covariant
|
covariant: Covariant = object()
|
||||||
contravariant: Contravariant
|
contravariant: Contravariant = object()
|
||||||
invariant: Invariant
|
invariant: Invariant = object()
|
||||||
coco: Coco
|
coco: Coco = object()
|
||||||
cocontra: Cocontra
|
cocontra: Cocontra = object()
|
||||||
contraco: Contraco
|
contraco: Contraco = object()
|
||||||
contracontra: Contracontra
|
contracontra: Contracontra = object()
|
||||||
t1: T1
|
t1: T1 = object()
|
||||||
t2: T2
|
t2: T2 = object()
|
||||||
|
|
||||||
# Dummy print to prudce judgements for the expressions
|
# Dummy print to prudce judgements for the expressions
|
||||||
print(
|
print(
|
||||||
@@ -36,17 +36,17 @@ print(
|
|||||||
t2,
|
t2,
|
||||||
)
|
)
|
||||||
|
|
||||||
cov1: Covariant[float]
|
cov1: Covariant[float] = object()
|
||||||
cov2: Covariant[int]
|
cov2: Covariant[int] = object()
|
||||||
cov1 = cov2 # Ok because int <: float => Covariant[int] <: Covariant[float]
|
cov1 = cov2 # Ok because int <: float => Covariant[int] <: Covariant[float]
|
||||||
cov2 = cov1 # Invalid
|
cov2 = cov1 # Invalid
|
||||||
|
|
||||||
contra1: Contravariant[float]
|
contra1: Contravariant[float] = object()
|
||||||
contra2: Contravariant[int]
|
contra2: Contravariant[int] = object()
|
||||||
contra1 = contra2 # Invalid
|
contra1 = contra2 # Invalid
|
||||||
contra2 = contra1 # Ok because int <: float => Covariant[float] <: Covariant[int]
|
contra2 = contra1 # Ok because int <: float => Covariant[float] <: Covariant[int]
|
||||||
|
|
||||||
inv1: Invariant[float]
|
inv1: Invariant[float] = object()
|
||||||
inv2: Invariant[int]
|
inv2: Invariant[int] = object()
|
||||||
inv1 = inv2 # Invalid
|
inv1 = inv2 # Invalid
|
||||||
inv2 = inv1 # Invalid
|
inv2 = inv1 # Invalid
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,9 @@
|
|||||||
# type: ignore
|
# type: ignore
|
||||||
# ruff: disable [F821]
|
# ruff: disable [F821]
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
df1: Frame[i:int, a:int, b:float]
|
df1 = cast(Frame[i:int, a:int, b:float], pd.DataFrame())
|
||||||
df2: Frame[i:int, a:int, b:float]
|
df2 = cast(Frame[i:int, a:int, b:float], pd.DataFrame())
|
||||||
|
|
||||||
_: Any
|
_: Any
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user