From 1a1b0e8e15d2e02299faa6b7fa62f80edaf8a6dc Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Sun, 31 May 2026 13:32:08 +0200 Subject: [PATCH] docs(checker): add documentation to checker, resolvers, etc. --- midas/checker/checker.py | 2 + midas/checker/environment.py | 84 ++++++++++++++++++++++++++++++++++-- midas/resolver/midas.py | 52 ++++++++++++++++++++++ midas/resolver/resolver.py | 45 +++++++++++++++++++ 4 files changed, 180 insertions(+), 3 deletions(-) diff --git a/midas/checker/checker.py b/midas/checker/checker.py index e0649a0..f83dd14 100644 --- a/midas/checker/checker.py +++ b/midas/checker/checker.py @@ -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 diff --git a/midas/checker/environment.py b/midas/checker/environment.py index fa6dfe2..39e53e3 100644 --- a/midas/checker/environment.py +++ b/midas/checker/environment.py @@ -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 diff --git a/midas/resolver/midas.py b/midas/resolver/midas.py index dadc6db..8fd1e66 100644 --- a/midas/resolver/midas.py +++ b/midas/resolver/midas.py @@ -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) diff --git a/midas/resolver/resolver.py b/midas/resolver/resolver.py index f09a54d..9d7581f 100644 --- a/midas/resolver/resolver.py +++ b/midas/resolver/resolver.py @@ -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)