docs(checker): add documentation to checker, resolvers, etc.
This commit is contained in:
@@ -32,6 +32,8 @@ class Checker(
|
|||||||
p.Expr.Visitor[Type],
|
p.Expr.Visitor[Type],
|
||||||
p.MidasType.Visitor[Type],
|
p.MidasType.Visitor[Type],
|
||||||
):
|
):
|
||||||
|
"""A type checker which can use custom type definitions"""
|
||||||
|
|
||||||
def __init__(self, locals: dict[p.Expr, int], file_path: Path):
|
def __init__(self, locals: dict[p.Expr, int], file_path: Path):
|
||||||
self.logger: logging.Logger = logging.getLogger("Checker")
|
self.logger: logging.Logger = logging.getLogger("Checker")
|
||||||
self.file_path: Path = file_path
|
self.file_path: Path = file_path
|
||||||
|
|||||||
@@ -6,15 +6,35 @@ from midas.checker.types import Type
|
|||||||
|
|
||||||
|
|
||||||
class Environment:
|
class Environment:
|
||||||
|
"""
|
||||||
|
A scoped environment in which variables are defined
|
||||||
|
|
||||||
|
Each environment can inherit from a parent/enclosing environment.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, enclosing: Optional[Environment] = None) -> None:
|
def __init__(self, enclosing: Optional[Environment] = None) -> None:
|
||||||
self.enclosing: Optional[Environment] = enclosing
|
self.enclosing: Optional[Environment] = enclosing
|
||||||
self.values: dict[str, Type] = {}
|
self.values: dict[str, Type] = {}
|
||||||
self.return_types: set[Type] = set()
|
self.return_types: set[Type] = set()
|
||||||
|
|
||||||
def define(self, name: str, value: Type):
|
def define(self, name: str, value: Type) -> None:
|
||||||
|
"""Define a variable in this environment
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): the name of the variable
|
||||||
|
value (Type): the value
|
||||||
|
"""
|
||||||
self.values[name] = value
|
self.values[name] = value
|
||||||
|
|
||||||
def get(self, name: str) -> Optional[Type]:
|
def get(self, name: str) -> Optional[Type]:
|
||||||
|
"""Get a variable in the closest environment which has a definition for it
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): the name of the variable
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[Type]: the value of the variable, or None if it was not found
|
||||||
|
"""
|
||||||
if name in self.values:
|
if name in self.values:
|
||||||
return self.values[name]
|
return self.values[name]
|
||||||
if self.enclosing is not None:
|
if self.enclosing is not None:
|
||||||
@@ -23,6 +43,15 @@ class Environment:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def assign(self, name: str, value: Type) -> bool:
|
def assign(self, name: str, value: Type) -> bool:
|
||||||
|
"""Assign a new value to a variable in the environment it was defined in
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): the name of the variable
|
||||||
|
value (Type): the new value
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the variable was assigned in this environment or an ancestor, False otherwise
|
||||||
|
"""
|
||||||
if name not in self.values:
|
if name not in self.values:
|
||||||
if self.enclosing is None:
|
if self.enclosing is None:
|
||||||
return False
|
return False
|
||||||
@@ -32,22 +61,71 @@ class Environment:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
|
"""Clear all definitions in this environment"""
|
||||||
self.values = {}
|
self.values = {}
|
||||||
|
|
||||||
def get_at(self, distance: int, name: str) -> Optional[Type]:
|
def get_at(self, distance: int, name: str) -> Optional[Type]:
|
||||||
|
"""Get the value of a variable at a given distance
|
||||||
|
|
||||||
|
A distance of 0 looks up in this environment, 1 in the parent environment, etc.
|
||||||
|
This methods expects `distance` to be valid. An error will be raised if
|
||||||
|
the stack does not extend far enough to reach `distance`
|
||||||
|
|
||||||
|
Args:
|
||||||
|
distance (int): the scope distance
|
||||||
|
name (str): the name of the variable
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[Type]: the value at the given distance, or None if it is not defined in that environment
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AssertionError: if the stack does not extend far enough to reach `distance`
|
||||||
|
"""
|
||||||
return self.ancestor(distance).values.get(name)
|
return self.ancestor(distance).values.get(name)
|
||||||
|
|
||||||
def assign_at(self, distance: int, name: str, value: Type):
|
def assign_at(self, distance: int, name: str, value: Type) -> None:
|
||||||
|
"""Assign a new value to a variable at a given distance
|
||||||
|
|
||||||
|
A distance of 0 assigns in this environment, 1 in the parent environment, etc.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
distance (int): the scope distance
|
||||||
|
name (str): the name of the variable
|
||||||
|
value (Type): the new value
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AssertionError: if the stack does not extend far enough to reach `distance`
|
||||||
|
"""
|
||||||
self.ancestor(distance).values[name] = value
|
self.ancestor(distance).values[name] = value
|
||||||
|
|
||||||
def ancestor(self, distance: int) -> Environment:
|
def ancestor(self, distance: int) -> Environment:
|
||||||
|
"""Get the ancestor at a given distance
|
||||||
|
|
||||||
|
A distance of 0 references this environment, 1 the parent environment, etc.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
distance (int): the scope distance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Environment: the environment
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AssertionError: if the stack does not extend far enough to reach `distance`
|
||||||
|
"""
|
||||||
env: Environment = self
|
env: Environment = self
|
||||||
for _ in range(distance):
|
for _ in range(distance):
|
||||||
assert env.enclosing is not None
|
assert env.enclosing is not None
|
||||||
env = env.enclosing
|
env = env.enclosing
|
||||||
return env
|
return env
|
||||||
|
|
||||||
def flat_dict(self) -> dict:
|
def flat_dict(self) -> dict[str, Type]:
|
||||||
|
"""Get the current environment including definitions in its ancestor as a flat dictionary
|
||||||
|
|
||||||
|
This method recursively combines this environment definitions with its ancestor's
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: the combined environment
|
||||||
|
"""
|
||||||
if self.enclosing is None:
|
if self.enclosing is None:
|
||||||
return self.values
|
return self.values
|
||||||
return self.enclosing.flat_dict() | self.values
|
return self.enclosing.flat_dict() | self.values
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ from midas.checker.types import BaseType, SimpleType, Type
|
|||||||
|
|
||||||
|
|
||||||
class MidasResolver(m.Stmt.Visitor[None], m.Expr.Visitor[Type]):
|
class MidasResolver(m.Stmt.Visitor[None], m.Expr.Visitor[Type]):
|
||||||
|
"""A resolver which evaluates Midas type definitions and build a registry"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._types: dict[str, Type] = {}
|
self._types: dict[str, Type] = {}
|
||||||
self._operations: dict[tuple[Type, str, Type], Type] = {}
|
self._operations: dict[tuple[Type, str, Type], Type] = {}
|
||||||
@@ -12,6 +14,17 @@ class MidasResolver(m.Stmt.Visitor[None], m.Expr.Visitor[Type]):
|
|||||||
self._define_builtin()
|
self._define_builtin()
|
||||||
|
|
||||||
def get_type(self, name: str) -> Type:
|
def get_type(self, name: str) -> Type:
|
||||||
|
"""Get a type from its name
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): the name of the type
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NameError: if the type is not defined
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the type
|
||||||
|
"""
|
||||||
type: Optional[Type] = self._types.get(name)
|
type: Optional[Type] = self._types.get(name)
|
||||||
if type is None:
|
if type is None:
|
||||||
raise NameError(f"Undefined type {name}")
|
raise NameError(f"Undefined type {name}")
|
||||||
@@ -20,11 +33,22 @@ class MidasResolver(m.Stmt.Visitor[None], m.Expr.Visitor[Type]):
|
|||||||
def get_operation_result(
|
def get_operation_result(
|
||||||
self, left: Type, operator: str, right: Type
|
self, left: Type, operator: str, right: Type
|
||||||
) -> Optional[Type]:
|
) -> Optional[Type]:
|
||||||
|
"""Get the resulting type of an operation
|
||||||
|
|
||||||
|
Args:
|
||||||
|
left (Type): the type of the left operand
|
||||||
|
operator (str): the operation name
|
||||||
|
right (Type): the type of the right operand
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[Type]: the result type, or None if no matching operation was found
|
||||||
|
"""
|
||||||
operation: tuple[Type, str, Type] = (left, operator, right)
|
operation: tuple[Type, str, Type] = (left, operator, right)
|
||||||
result: Optional[Type] = self._operations.get(operation)
|
result: Optional[Type] = self._operations.get(operation)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _define_builtin(self):
|
def _define_builtin(self):
|
||||||
|
"""Define builtin types and operations"""
|
||||||
self.define_type("bool", BaseType(name="bool"))
|
self.define_type("bool", BaseType(name="bool"))
|
||||||
self.define_type("int", BaseType(name="int"))
|
self.define_type("int", BaseType(name="int"))
|
||||||
self.define_type("float", BaseType(name="float"))
|
self.define_type("float", BaseType(name="float"))
|
||||||
@@ -37,12 +61,35 @@ class MidasResolver(m.Stmt.Visitor[None], m.Expr.Visitor[Type]):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def define_type(self, name: str, type: Type) -> Type:
|
def define_type(self, name: str, type: Type) -> Type:
|
||||||
|
"""Define a type in the registry
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): the name of the type
|
||||||
|
type (Type): the type to define
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: if a type is already defined with that name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the defined type
|
||||||
|
"""
|
||||||
if name in self._types:
|
if name in self._types:
|
||||||
raise ValueError(f"Type {name} already defined")
|
raise ValueError(f"Type {name} already defined")
|
||||||
self._types[name] = type
|
self._types[name] = type
|
||||||
return type
|
return type
|
||||||
|
|
||||||
def define_operation(self, left: Type, operator: str, right: Type, result: Type):
|
def define_operation(self, left: Type, operator: str, right: Type, result: Type):
|
||||||
|
"""Define an operation in the registry
|
||||||
|
|
||||||
|
Args:
|
||||||
|
left (Type): the type of the left operand
|
||||||
|
operator (str): the operation name
|
||||||
|
right (Type): the type of the right operand
|
||||||
|
result (Type): the result type
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: if an operation is already defined with these operands and name
|
||||||
|
"""
|
||||||
operation: tuple[Type, str, Type] = (left, operator, right)
|
operation: tuple[Type, str, Type] = (left, operator, right)
|
||||||
if operation in self._operations:
|
if operation in self._operations:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@@ -51,6 +98,11 @@ class MidasResolver(m.Stmt.Visitor[None], m.Expr.Visitor[Type]):
|
|||||||
self._operations[operation] = result
|
self._operations[operation] = result
|
||||||
|
|
||||||
def resolve(self, stmts: list[m.Stmt]):
|
def resolve(self, stmts: list[m.Stmt]):
|
||||||
|
"""Process a sequence of statements
|
||||||
|
|
||||||
|
Args:
|
||||||
|
stmts (list[m.Stmt]): the statements
|
||||||
|
"""
|
||||||
for stmt in stmts:
|
for stmt in stmts:
|
||||||
stmt.accept(self)
|
stmt.accept(self)
|
||||||
|
|
||||||
|
|||||||
@@ -5,21 +5,41 @@ class ResolverError(Exception): ...
|
|||||||
|
|
||||||
|
|
||||||
class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
||||||
|
"""A variable assignment and reference resolver
|
||||||
|
|
||||||
|
This class keeps track of which scope a variable is defined in and which
|
||||||
|
scope is referred to when a variable is referenced
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.locals: dict[p.Expr, int] = {}
|
self.locals: dict[p.Expr, int] = {}
|
||||||
self.scopes: list[dict[str, bool]] = []
|
self.scopes: list[dict[str, bool]] = []
|
||||||
|
|
||||||
def resolve(self, *objects: p.Stmt | p.Expr) -> None:
|
def resolve(self, *objects: p.Stmt | p.Expr) -> None:
|
||||||
|
"""Resolve the given statements or expressions"""
|
||||||
|
|
||||||
for obj in objects:
|
for obj in objects:
|
||||||
obj.accept(self)
|
obj.accept(self)
|
||||||
|
|
||||||
def begin_scope(self):
|
def begin_scope(self):
|
||||||
|
"""Begin a new scope inside the current one"""
|
||||||
self.scopes.append({})
|
self.scopes.append({})
|
||||||
|
|
||||||
def end_scope(self):
|
def end_scope(self):
|
||||||
|
"""Close the current scope"""
|
||||||
self.scopes.pop()
|
self.scopes.pop()
|
||||||
|
|
||||||
def declare(self, name: str) -> None:
|
def declare(self, name: str) -> None:
|
||||||
|
"""Declare a variable in the current scope
|
||||||
|
|
||||||
|
This method must be called *before* evaluating the variable initializer
|
||||||
|
|
||||||
|
Args:
|
||||||
|
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]
|
||||||
@@ -30,17 +50,42 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
|
|||||||
scope[name] = False
|
scope[name] = False
|
||||||
|
|
||||||
def define(self, name: str) -> None:
|
def define(self, name: str) -> None:
|
||||||
|
"""Define a variable in the current scope
|
||||||
|
|
||||||
|
This method must be called *after* evaluating the variable initializer
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): the name of the variable
|
||||||
|
"""
|
||||||
if len(self.scopes) == 0:
|
if len(self.scopes) == 0:
|
||||||
return
|
return
|
||||||
self.scopes[-1][name] = True
|
self.scopes[-1][name] = True
|
||||||
|
|
||||||
def resolve_local(self, expr: p.Expr, name: str) -> None:
|
def resolve_local(self, expr: p.Expr, name: str) -> None:
|
||||||
|
"""Resolve a variable reference and store the scope distance
|
||||||
|
|
||||||
|
This method associates to the variable expression a number representing
|
||||||
|
the "distance" of the variable declaration, i.e. the number of scope
|
||||||
|
levels to go "up" to find the closest declaration for that variable.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
expr (p.Expr): the variable expression
|
||||||
|
name (str): the name of the variable
|
||||||
|
"""
|
||||||
for i, scope in enumerate(reversed(self.scopes)):
|
for i, scope in enumerate(reversed(self.scopes)):
|
||||||
if name in scope:
|
if name in scope:
|
||||||
self.locals[expr] = i
|
self.locals[expr] = i
|
||||||
return
|
return
|
||||||
|
|
||||||
def resolve_function(self, function: p.Function) -> None:
|
def resolve_function(self, function: p.Function) -> None:
|
||||||
|
"""Resolve a function definition
|
||||||
|
|
||||||
|
This method creates a new scope for the function, resolves all the
|
||||||
|
parameter declarations and then the body.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
function (p.Function): the function to resolve
|
||||||
|
"""
|
||||||
self.begin_scope()
|
self.begin_scope()
|
||||||
for param in function.all_args:
|
for param in function.all_args:
|
||||||
self.declare(param.name)
|
self.declare(param.name)
|
||||||
|
|||||||
Reference in New Issue
Block a user