docs: add docstrings for type checkers
This commit is contained in:
@@ -58,10 +58,21 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[Type], m.Type.Visitor[Type
|
|||||||
self._preamble: Environment = Preamble(self.types)
|
self._preamble: Environment = Preamble(self.types)
|
||||||
|
|
||||||
def set_reporter(self, reporter: FileReporter):
|
def set_reporter(self, reporter: FileReporter):
|
||||||
|
"""Set the file reporter to use for diagnostics
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reporter (FileReporter): the file reporter
|
||||||
|
"""
|
||||||
self.reporter = reporter
|
self.reporter = reporter
|
||||||
self.dispatcher.set_reporter(reporter)
|
self.dispatcher.set_reporter(reporter)
|
||||||
|
|
||||||
def process(self, source: str, path: Optional[str]):
|
def process(self, source: str, path: Optional[str]):
|
||||||
|
"""Process some Midas source code
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source (str): the Midas source code
|
||||||
|
path (Optional[str]): the path of the source file, if known
|
||||||
|
"""
|
||||||
reporter: FileReporter = self.reporter.for_file(path)
|
reporter: FileReporter = self.reporter.for_file(path)
|
||||||
self.set_reporter(reporter)
|
self.set_reporter(reporter)
|
||||||
|
|
||||||
@@ -74,6 +85,14 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[Type], m.Type.Visitor[Type
|
|||||||
self.resolve(stmts)
|
self.resolve(stmts)
|
||||||
|
|
||||||
def type_of(self, expr: m.Expr) -> Type:
|
def type_of(self, expr: m.Expr) -> Type:
|
||||||
|
"""Compute the type of the given expression
|
||||||
|
|
||||||
|
Args:
|
||||||
|
expr (m.Expr): the expression to type
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the type of the expression
|
||||||
|
"""
|
||||||
type: Type = expr.accept(self)
|
type: Type = expr.accept(self)
|
||||||
return type
|
return type
|
||||||
|
|
||||||
@@ -94,6 +113,21 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[Type], m.Type.Visitor[Type
|
|||||||
return self.types.get_type(name)
|
return self.types.get_type(name)
|
||||||
|
|
||||||
def get_variable(self, name: str) -> Type:
|
def get_variable(self, name: str) -> Type:
|
||||||
|
"""Get the type of a variable
|
||||||
|
|
||||||
|
This function will first look into the current predicate's parameters if
|
||||||
|
we are in a predicate definition.
|
||||||
|
The the variable is looked up in the preamble (i.e. global environment)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): the name of the variable
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NameError: if the variable cannot be found
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the type of the variable
|
||||||
|
"""
|
||||||
if name in self._predicate_params:
|
if name in self._predicate_params:
|
||||||
return self._predicate_params[name]
|
return self._predicate_params[name]
|
||||||
predicate: Optional[Predicate] = self.types.lookup_predicate(name)
|
predicate: Optional[Predicate] = self.types.lookup_predicate(name)
|
||||||
@@ -121,6 +155,11 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[Type], m.Type.Visitor[Type
|
|||||||
self.types._types[name] = inferrer.infer(type)
|
self.types._types[name] = inferrer.infer(type)
|
||||||
|
|
||||||
def assert_bool(self, expr: m.Expr):
|
def assert_bool(self, expr: m.Expr):
|
||||||
|
"""Check that the given expression is a subtype of `bool` or report an error
|
||||||
|
|
||||||
|
Args:
|
||||||
|
expr (m.Expr): the expression to check
|
||||||
|
"""
|
||||||
type: Type = self.type_of(expr)
|
type: Type = self.type_of(expr)
|
||||||
if not self.types.is_subtype(type, self._bool):
|
if not self.types.is_subtype(type, self._bool):
|
||||||
self.reporter.error(expr.location, f"Must be a boolean but is {type}")
|
self.reporter.error(expr.location, f"Must be a boolean but is {type}")
|
||||||
@@ -197,6 +236,16 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[Type], m.Type.Visitor[Type
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _is_valid_predicate(self, body: Type) -> bool:
|
def _is_valid_predicate(self, body: Type) -> bool:
|
||||||
|
"""Check whether the given type is valid as a predicate's body
|
||||||
|
|
||||||
|
Accepted types are either subtypes of `bool` or valid predicates
|
||||||
|
|
||||||
|
Args:
|
||||||
|
body (Type): the potential predicate body
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: `True` if `body` can be a predicate body, `False` otherwise
|
||||||
|
"""
|
||||||
match body:
|
match body:
|
||||||
case Function(returns=returns):
|
case Function(returns=returns):
|
||||||
return self._is_valid_predicate(returns)
|
return self._is_valid_predicate(returns)
|
||||||
@@ -222,7 +271,11 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[Type], m.Type.Visitor[Type
|
|||||||
return self._visit_binary_expr(expr.location, expr.left, expr.right, method)
|
return self._visit_binary_expr(expr.location, expr.left, expr.right, method)
|
||||||
|
|
||||||
def _visit_binary_expr(
|
def _visit_binary_expr(
|
||||||
self, location: Location, left_expr: m.Expr, right_expr: m.Expr, method: str
|
self,
|
||||||
|
location: Location,
|
||||||
|
left_expr: m.Expr,
|
||||||
|
right_expr: m.Expr,
|
||||||
|
method: str,
|
||||||
) -> Type:
|
) -> Type:
|
||||||
left: Type = self.type_of(left_expr)
|
left: Type = self.type_of(left_expr)
|
||||||
right: Type = self.type_of(right_expr)
|
right: Type = self.type_of(right_expr)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import ast
|
import ast
|
||||||
import logging
|
import logging
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
import midas.ast.python as p
|
import midas.ast.python as p
|
||||||
@@ -56,19 +55,6 @@ class UndefinedMethodException(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
|
||||||
class MappedArgument:
|
|
||||||
expr: p.Expr
|
|
||||||
type: Type
|
|
||||||
argument: Function.Parameter
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
|
||||||
class OverloadCandidate:
|
|
||||||
function: Function
|
|
||||||
mapped: list[MappedArgument]
|
|
||||||
|
|
||||||
|
|
||||||
class PythonTyper(
|
class PythonTyper(
|
||||||
p.Stmt.Visitor[None],
|
p.Stmt.Visitor[None],
|
||||||
p.Expr.Visitor[Type],
|
p.Expr.Visitor[Type],
|
||||||
@@ -97,10 +83,24 @@ class PythonTyper(
|
|||||||
self.assertions: AssertionCollector = AssertionCollector()
|
self.assertions: AssertionCollector = AssertionCollector()
|
||||||
|
|
||||||
def set_reporter(self, reporter: FileReporter):
|
def set_reporter(self, reporter: FileReporter):
|
||||||
|
"""Set the file reporter to use for diagnostics
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reporter (FileReporter): the file reporter
|
||||||
|
"""
|
||||||
self.reporter = reporter
|
self.reporter = reporter
|
||||||
self.dispatcher.set_reporter(self.reporter)
|
self.dispatcher.set_reporter(self.reporter)
|
||||||
|
|
||||||
def process(self, source: str, path: Optional[str]) -> TypedAST:
|
def process(self, source: str, path: Optional[str]) -> TypedAST:
|
||||||
|
"""Process some Python source code
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source (str): the Python source code
|
||||||
|
path (Optional[str]): the path of the source file, if known
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
TypedAST: all generated typechecking information
|
||||||
|
"""
|
||||||
reporter: FileReporter = self.reporter.for_file(path)
|
reporter: FileReporter = self.reporter.for_file(path)
|
||||||
self.set_reporter(reporter)
|
self.set_reporter(reporter)
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ class PythonTyper(
|
|||||||
)
|
)
|
||||||
|
|
||||||
def judge(self, expr: p.Expr, type: Type):
|
def judge(self, expr: p.Expr, type: Type):
|
||||||
"""Record a typing judgement
|
"""Record a typing judgement for the given expression
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
expr (p.Expr): the judged expression
|
expr (p.Expr): the judged expression
|
||||||
@@ -134,7 +134,7 @@ class PythonTyper(
|
|||||||
self.judgements.append((expr, type))
|
self.judgements.append((expr, type))
|
||||||
|
|
||||||
def compute_type(self, expr: p.Expr) -> Type:
|
def compute_type(self, expr: p.Expr) -> Type:
|
||||||
"""Evaluate the type of an expression
|
"""Evaluate the type of the given expression
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
expr (p.Expr): the expression to type
|
expr (p.Expr): the expression to type
|
||||||
@@ -145,7 +145,7 @@ class PythonTyper(
|
|||||||
return expr.accept(self)
|
return expr.accept(self)
|
||||||
|
|
||||||
def type_of(self, expr: p.Expr) -> Type:
|
def type_of(self, expr: p.Expr) -> Type:
|
||||||
"""Evaluate the type of an expression and record the judgement
|
"""Evaluate the type of the given expression and record the judgement
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
expr (p.Expr): the expression to evaluate
|
expr (p.Expr): the expression to evaluate
|
||||||
@@ -158,9 +158,22 @@ class PythonTyper(
|
|||||||
return type
|
return type
|
||||||
|
|
||||||
def resolve_type_expr(self, expr: p.MidasType) -> Type:
|
def resolve_type_expr(self, expr: p.MidasType) -> Type:
|
||||||
|
"""Resolve the type of a type expression (annotation)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
expr (p.MidasType): the type expression
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the resolved type
|
||||||
|
"""
|
||||||
return expr.accept(self)
|
return expr.accept(self)
|
||||||
|
|
||||||
def process_stmt(self, stmt: p.Stmt) -> None:
|
def process_stmt(self, stmt: p.Stmt) -> None:
|
||||||
|
"""Type check the given statement
|
||||||
|
|
||||||
|
Args:
|
||||||
|
stmt (p.Stmt): the statement to type-check
|
||||||
|
"""
|
||||||
stmt.accept(self)
|
stmt.accept(self)
|
||||||
|
|
||||||
def process_block(self, block: list[p.Stmt], env: Environment) -> bool:
|
def process_block(self, block: list[p.Stmt], env: Environment) -> bool:
|
||||||
@@ -224,6 +237,24 @@ class PythonTyper(
|
|||||||
positional: list[TypedExpr],
|
positional: list[TypedExpr],
|
||||||
keywords: dict[str, TypedExpr],
|
keywords: dict[str, TypedExpr],
|
||||||
) -> Type:
|
) -> Type:
|
||||||
|
"""Evaluate a method call on an object
|
||||||
|
|
||||||
|
Calls to dataframes and columns types are delegated to the appropriate manager
|
||||||
|
|
||||||
|
Args:
|
||||||
|
location (Location): the location of the call
|
||||||
|
call_expr (p.Expr): the call expression
|
||||||
|
obj (TypedExpr): the object on which the method is called
|
||||||
|
method_name (str): the method name
|
||||||
|
positional (list[TypedExpr]): the list of positional arguments
|
||||||
|
keywords (dict[str, TypedExpr]): the map of keyword arguments
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
UndefinedMethodException: if the method is not defined
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type: the return type of the call
|
||||||
|
"""
|
||||||
unfolded: Type = unfold_type(obj[1])
|
unfolded: Type = unfold_type(obj[1])
|
||||||
match unfolded:
|
match unfolded:
|
||||||
case DataFrameType():
|
case DataFrameType():
|
||||||
@@ -283,6 +314,15 @@ class PythonTyper(
|
|||||||
return result.result
|
return result.result
|
||||||
|
|
||||||
def is_subtype(self, type1: Type, type2: Type) -> bool:
|
def is_subtype(self, type1: Type, type2: Type) -> bool:
|
||||||
|
"""Check whether `type1` is a subtype of `type2`
|
||||||
|
|
||||||
|
Args:
|
||||||
|
type1 (Type): the potential "subtype"
|
||||||
|
type2 (Type): the potential "supertype"
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: whether `type1` is a subtype of `type2`
|
||||||
|
"""
|
||||||
return self.types.is_subtype(type1, type2)
|
return self.types.is_subtype(type1, type2)
|
||||||
|
|
||||||
def visit_expression_stmt(self, stmt: p.ExpressionStmt) -> None:
|
def visit_expression_stmt(self, stmt: p.ExpressionStmt) -> None:
|
||||||
@@ -408,6 +448,15 @@ class PythonTyper(
|
|||||||
self._assign(stmt.location, target, value_type)
|
self._assign(stmt.location, target, value_type)
|
||||||
|
|
||||||
def _assign(self, location: Location, target: p.Expr, value_type: Type):
|
def _assign(self, location: Location, target: p.Expr, value_type: Type):
|
||||||
|
"""Handle an assignment to the given target
|
||||||
|
|
||||||
|
Delegate to the appropriate method according to the target type
|
||||||
|
|
||||||
|
Args:
|
||||||
|
location (Location): the location of the assignment
|
||||||
|
target (p.Expr): the assignment's target
|
||||||
|
value_type (Type): the value to be assigned
|
||||||
|
"""
|
||||||
match target:
|
match target:
|
||||||
case p.VariableExpr():
|
case p.VariableExpr():
|
||||||
self._assign_var(location, target, value_type)
|
self._assign_var(location, target, value_type)
|
||||||
@@ -429,6 +478,13 @@ class PythonTyper(
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _assign_var(self, location: Location, target: p.VariableExpr, value_type: Type):
|
def _assign_var(self, location: Location, target: p.VariableExpr, value_type: Type):
|
||||||
|
"""Type check assignment to the given target
|
||||||
|
|
||||||
|
Args:
|
||||||
|
location (Location): the location of the assignment
|
||||||
|
target (p.VariableExpr): the assignment's target
|
||||||
|
value_type (Type): the value to be assigned
|
||||||
|
"""
|
||||||
name: str = target.name
|
name: str = target.name
|
||||||
var_type: Optional[Type] = self.look_up_variable(name, target)
|
var_type: Optional[Type] = self.look_up_variable(name, target)
|
||||||
|
|
||||||
@@ -447,6 +503,13 @@ class PythonTyper(
|
|||||||
def _assign_attr(
|
def _assign_attr(
|
||||||
self, location: Location, object: p.Expr, name: str, value_type: Type
|
self, location: Location, object: p.Expr, name: str, value_type: Type
|
||||||
):
|
):
|
||||||
|
"""Type check assignment to the given target
|
||||||
|
|
||||||
|
Args:
|
||||||
|
location (Location): the location of the assignment
|
||||||
|
target (p.VariableExpr): the assignment's target
|
||||||
|
value_type (Type): the value to be assigned
|
||||||
|
"""
|
||||||
object_type: Type = self.type_of(object)
|
object_type: Type = self.type_of(object)
|
||||||
member: Optional[Type] = self.types.lookup_member(object_type, name)
|
member: Optional[Type] = self.types.lookup_member(object_type, name)
|
||||||
if member is None:
|
if member is None:
|
||||||
@@ -466,6 +529,13 @@ class PythonTyper(
|
|||||||
index: p.Expr,
|
index: p.Expr,
|
||||||
value_type: Type,
|
value_type: Type,
|
||||||
):
|
):
|
||||||
|
"""Type check assignment to the given target
|
||||||
|
|
||||||
|
Args:
|
||||||
|
location (Location): the location of the assignment
|
||||||
|
target (p.VariableExpr): the assignment's target
|
||||||
|
value_type (Type): the value to be assigned
|
||||||
|
"""
|
||||||
var_type: Type = self.type_of(var)
|
var_type: Type = self.type_of(var)
|
||||||
unfolded_type: Type = unfold_type(var_type)
|
unfolded_type: Type = unfold_type(var_type)
|
||||||
# TODO: what happens if type is an alias of a dataframe type
|
# TODO: what happens if type is an alias of a dataframe type
|
||||||
@@ -887,6 +957,15 @@ class PythonTyper(
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _get_iterator_type(self, expr: p.Expr, type: Type) -> Optional[Type]:
|
def _get_iterator_type(self, expr: p.Expr, type: Type) -> Optional[Type]:
|
||||||
|
"""Get the item type of an iterator type
|
||||||
|
|
||||||
|
Args:
|
||||||
|
expr (p.Expr): the iterator expression
|
||||||
|
type (Type): the iterator type
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[Type]: the item type, or `None` if it cannot be determined
|
||||||
|
"""
|
||||||
# TODO: lookup __iter__
|
# TODO: lookup __iter__
|
||||||
getitem: Optional[Type] = self.types.lookup_member(type, "__getitem__")
|
getitem: Optional[Type] = self.types.lookup_member(type, "__getitem__")
|
||||||
if getitem is None:
|
if getitem is None:
|
||||||
@@ -906,6 +985,16 @@ class PythonTyper(
|
|||||||
return result.result
|
return result.result
|
||||||
|
|
||||||
def define_typevar(self, call: p.CallExpr) -> Optional[TypeVar]:
|
def define_typevar(self, call: p.CallExpr) -> Optional[TypeVar]:
|
||||||
|
"""Define a type variable from a call to `typing.TypeVar`
|
||||||
|
|
||||||
|
Args:
|
||||||
|
call (p.CallExpr): the call to `typing.TypeVar`
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[TypeVar]: the define type variable, or `None` if the call
|
||||||
|
is invalid
|
||||||
|
"""
|
||||||
|
|
||||||
def is_kw_true(name: str) -> bool:
|
def is_kw_true(name: str) -> bool:
|
||||||
match call.keywords.get(name):
|
match call.keywords.get(name):
|
||||||
case p.LiteralExpr(value=True):
|
case p.LiteralExpr(value=True):
|
||||||
@@ -948,6 +1037,19 @@ class PythonTyper(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def _parse_type_from_expr(self, expr: p.Expr) -> p.MidasType:
|
def _parse_type_from_expr(self, expr: p.Expr) -> p.MidasType:
|
||||||
|
"""Parse a type expression from a raw expression
|
||||||
|
|
||||||
|
This is useful for expressions inside a `TypeVar`'s `bound` parameter
|
||||||
|
|
||||||
|
Args:
|
||||||
|
expr (p.Expr): the expression to parse
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NotImplementedError: if the expression is not supported
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
p.MidasType: the parsed type node
|
||||||
|
"""
|
||||||
location: Location = expr.location
|
location: Location = expr.location
|
||||||
parser = PythonParser()
|
parser = PythonParser()
|
||||||
match expr:
|
match expr:
|
||||||
@@ -960,6 +1062,16 @@ class PythonTyper(
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def _get_literal(self, expr: p.Expr) -> tuple[bool, Any]:
|
def _get_literal(self, expr: p.Expr) -> tuple[bool, Any]:
|
||||||
|
"""Get the literal value of a literal-like expression
|
||||||
|
|
||||||
|
Args:
|
||||||
|
expr (p.Expr): the expression
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[bool, Any]: a tuple containing a boolean indicating whether
|
||||||
|
the given expression is literal-like, and the literal value (or
|
||||||
|
`None` if the first value is `False`)
|
||||||
|
"""
|
||||||
match expr:
|
match expr:
|
||||||
case p.LiteralExpr(value=value):
|
case p.LiteralExpr(value=value):
|
||||||
return True, value
|
return True, value
|
||||||
@@ -1016,6 +1128,17 @@ class PythonTyper(
|
|||||||
def _evaluate_cast_statically(
|
def _evaluate_cast_statically(
|
||||||
self, expr: p.CastExpr, subject_type: Type, target_type: Type, lit_value: Any
|
self, expr: p.CastExpr, subject_type: Type, target_type: Type, lit_value: Any
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
"""Evaluate the given cast expression statically
|
||||||
|
|
||||||
|
Args:
|
||||||
|
expr (p.CastExpr): the cast expression
|
||||||
|
subject_type (Type): the subject type being casted
|
||||||
|
target_type (Type): the target type to which the expression is casted
|
||||||
|
lit_value (Any): the literal value of the expression
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: whether the cast expression could be evaluated successfully
|
||||||
|
"""
|
||||||
match target_type:
|
match target_type:
|
||||||
case TopType():
|
case TopType():
|
||||||
return True
|
return True
|
||||||
|
|||||||
Reference in New Issue
Block a user