docs(checker): add documentation to checker, resolvers, etc.

This commit is contained in:
2026-05-31 13:32:08 +02:00
parent 4ddde364ed
commit 1a1b0e8e15
4 changed files with 180 additions and 3 deletions

View File

@@ -32,6 +32,8 @@ class Checker(
p.Expr.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):
self.logger: logging.Logger = logging.getLogger("Checker")
self.file_path: Path = file_path

View File

@@ -6,15 +6,35 @@ from midas.checker.types import Type
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:
self.enclosing: Optional[Environment] = enclosing
self.values: dict[str, Type] = {}
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
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:
return self.values[name]
if self.enclosing is not None:
@@ -23,6 +43,15 @@ class Environment:
return None
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 self.enclosing is None:
return False
@@ -32,22 +61,71 @@ class Environment:
return True
def clear(self):
"""Clear all definitions in this environment"""
self.values = {}
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)
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
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
for _ in range(distance):
assert env.enclosing is not None
env = env.enclosing
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:
return self.values
return self.enclosing.flat_dict() | self.values

View File

@@ -5,6 +5,8 @@ from midas.checker.types import BaseType, SimpleType, 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:
self._types: dict[str, 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()
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)
if type is None:
raise NameError(f"Undefined type {name}")
@@ -20,11 +33,22 @@ class MidasResolver(m.Stmt.Visitor[None], m.Expr.Visitor[Type]):
def get_operation_result(
self, left: Type, operator: str, right: 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)
result: Optional[Type] = self._operations.get(operation)
return result
def _define_builtin(self):
"""Define builtin types and operations"""
self.define_type("bool", BaseType(name="bool"))
self.define_type("int", BaseType(name="int"))
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:
"""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:
raise ValueError(f"Type {name} already defined")
self._types[name] = type
return 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)
if operation in self._operations:
raise ValueError(
@@ -51,6 +98,11 @@ class MidasResolver(m.Stmt.Visitor[None], m.Expr.Visitor[Type]):
self._operations[operation] = result
def resolve(self, stmts: list[m.Stmt]):
"""Process a sequence of statements
Args:
stmts (list[m.Stmt]): the statements
"""
for stmt in stmts:
stmt.accept(self)

View File

@@ -5,21 +5,41 @@ class ResolverError(Exception): ...
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):
self.locals: dict[p.Expr, int] = {}
self.scopes: list[dict[str, bool]] = []
def resolve(self, *objects: p.Stmt | p.Expr) -> None:
"""Resolve the given statements or expressions"""
for obj in objects:
obj.accept(self)
def begin_scope(self):
"""Begin a new scope inside the current one"""
self.scopes.append({})
def end_scope(self):
"""Close the current scope"""
self.scopes.pop()
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:
return
scope: dict[str, bool] = self.scopes[-1]
@@ -30,17 +50,42 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
scope[name] = False
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:
return
self.scopes[-1][name] = True
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)):
if name in scope:
self.locals[expr] = i
return
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()
for param in function.all_args:
self.declare(param.name)