fix(resolver): properly check if variable is defined

This commit is contained in:
2026-07-08 18:42:28 +02:00
parent 2118c260ab
commit 1b5691dca7
6 changed files with 2103 additions and 900 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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