Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1c217a335
|
||
|
|
5b3e87afcb
|
||
|
|
894d5a7196
|
||
|
|
eb809c6341
|
||
|
|
bd68d1003f
|
||
|
|
72c9236650
|
||
|
|
90051c7981
|
||
|
|
dd1e2e693c
|
@@ -0,0 +1,153 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import TYPE_CHECKING, Any, Callable, Optional
|
||||||
|
|
||||||
|
from midas.ast.location import Location
|
||||||
|
from midas.checker.registry import TypesRegistry
|
||||||
|
from midas.checker.reporter import FileReporter
|
||||||
|
from midas.checker.types import (
|
||||||
|
ColumnType,
|
||||||
|
DataFrameType,
|
||||||
|
Function,
|
||||||
|
Type,
|
||||||
|
UnknownType,
|
||||||
|
unfold_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from midas.checker.python import PythonTyper, TypedExpr
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def frame_method(*names: str):
|
||||||
|
def wrapper(func):
|
||||||
|
names_: tuple[str, ...] = names
|
||||||
|
if len(names_) == 0:
|
||||||
|
names_ = (func.__name__,)
|
||||||
|
setattr(func, "__method_names__", names_)
|
||||||
|
return func
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class Call:
|
||||||
|
location: Location
|
||||||
|
frame: DataFrameType
|
||||||
|
positional: list[TypedExpr]
|
||||||
|
keywords: dict[str, TypedExpr]
|
||||||
|
|
||||||
|
|
||||||
|
class _MethodRegistryMeta(type):
|
||||||
|
_methods: dict[str, Callable] = {}
|
||||||
|
|
||||||
|
def __new__(
|
||||||
|
cls,
|
||||||
|
name: str,
|
||||||
|
bases: tuple[type, ...],
|
||||||
|
namespace: dict[str, Any],
|
||||||
|
):
|
||||||
|
new_class = super().__new__(cls, name, bases, namespace)
|
||||||
|
new_class._methods = {}
|
||||||
|
for attr in namespace.values():
|
||||||
|
if callable(attr) and hasattr(attr, "__method_names__"):
|
||||||
|
for name in attr.__method_names__: # type: ignore
|
||||||
|
new_class._methods[name] = attr
|
||||||
|
return new_class
|
||||||
|
|
||||||
|
|
||||||
|
class MethodRegistry(metaclass=_MethodRegistryMeta):
|
||||||
|
def __init__(self, typer: PythonTyper) -> None:
|
||||||
|
self.typer: PythonTyper = typer
|
||||||
|
|
||||||
|
@property
|
||||||
|
def reporter(self) -> FileReporter:
|
||||||
|
return self.typer.reporter
|
||||||
|
|
||||||
|
@property
|
||||||
|
def types(self) -> TypesRegistry:
|
||||||
|
return self.typer.types
|
||||||
|
|
||||||
|
def call(
|
||||||
|
self,
|
||||||
|
method: str,
|
||||||
|
call: Call,
|
||||||
|
) -> Type:
|
||||||
|
func: Optional[Callable] = self._methods.get(method)
|
||||||
|
if func is None:
|
||||||
|
self.reporter.error(call.location, f"Unknown method {method}")
|
||||||
|
return UnknownType()
|
||||||
|
return func(self, call)
|
||||||
|
|
||||||
|
@frame_method("add", "__add__")
|
||||||
|
def add(
|
||||||
|
self,
|
||||||
|
call: Call,
|
||||||
|
) -> Type:
|
||||||
|
new_columns: list[DataFrameType.Column] = []
|
||||||
|
|
||||||
|
by_name: dict[str, DataFrameType.Column] = {}
|
||||||
|
frame2: Optional[DataFrameType] = None
|
||||||
|
if len(call.positional) != 0:
|
||||||
|
other: Type = call.positional[0][1]
|
||||||
|
unfolded_other: Type = unfold_type(other)
|
||||||
|
if isinstance(unfolded_other, DataFrameType):
|
||||||
|
frame2 = unfolded_other
|
||||||
|
by_name = {
|
||||||
|
col.name: col for col in frame2.columns if col.name is not None
|
||||||
|
}
|
||||||
|
|
||||||
|
in_frame1: set[str] = set()
|
||||||
|
for column in call.frame.columns:
|
||||||
|
if column.name is not None:
|
||||||
|
in_frame1.add(column.name)
|
||||||
|
|
||||||
|
col_type1: Type = column.type
|
||||||
|
col_type: Type = ColumnType(type=UnknownType())
|
||||||
|
if column.name in by_name:
|
||||||
|
column2 = by_name[column.name]
|
||||||
|
col_type2: Type = column2.type
|
||||||
|
if self.types.are_equivalent(col_type2, col_type1):
|
||||||
|
col_type = col_type1
|
||||||
|
|
||||||
|
new_column = DataFrameType.Column(
|
||||||
|
index=column.index,
|
||||||
|
name=column.name,
|
||||||
|
type=col_type,
|
||||||
|
)
|
||||||
|
new_columns.append(new_column)
|
||||||
|
|
||||||
|
if frame2 is not None:
|
||||||
|
for column in frame2.columns:
|
||||||
|
if column.name in in_frame1:
|
||||||
|
continue
|
||||||
|
new_columns.append(
|
||||||
|
DataFrameType.Column(
|
||||||
|
index=len(new_columns),
|
||||||
|
name=column.name,
|
||||||
|
type=ColumnType(type=UnknownType()),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
signature = Function(
|
||||||
|
args=[
|
||||||
|
Function.Argument(
|
||||||
|
pos=0,
|
||||||
|
name="other",
|
||||||
|
type=DataFrameType(columns=[]),
|
||||||
|
required=True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
returns=DataFrameType(columns=new_columns),
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
self.typer._get_call_result(
|
||||||
|
location=call.location,
|
||||||
|
callee=signature,
|
||||||
|
positional=call.positional,
|
||||||
|
keywords=call.keywords,
|
||||||
|
)
|
||||||
|
or UnknownType()
|
||||||
|
)
|
||||||
+26
-5
@@ -1,11 +1,15 @@
|
|||||||
from typing import Optional, TypeGuard, cast
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Optional, TypeGuard, cast
|
||||||
|
|
||||||
|
import midas.ast.python as p
|
||||||
from midas.ast.location import Location
|
from midas.ast.location import Location
|
||||||
from midas.checker.registry import TypesRegistry
|
from midas.checker.frame_methods import Call, MethodRegistry
|
||||||
from midas.checker.reporter import FileReporter
|
from midas.checker.reporter import FileReporter
|
||||||
from midas.checker.types import ColumnType, DataFrameType, TupleType, Type, UnknownType
|
from midas.checker.types import ColumnType, DataFrameType, TupleType, Type, UnknownType
|
||||||
|
|
||||||
import midas.ast.python as p
|
if TYPE_CHECKING:
|
||||||
|
from midas.checker.python import PythonTyper, TypedExpr
|
||||||
|
|
||||||
|
|
||||||
def is_list_of_literals(exprs: list[p.Expr]) -> TypeGuard[list[p.LiteralExpr]]:
|
def is_list_of_literals(exprs: list[p.Expr]) -> TypeGuard[list[p.LiteralExpr]]:
|
||||||
@@ -13,8 +17,9 @@ def is_list_of_literals(exprs: list[p.Expr]) -> TypeGuard[list[p.LiteralExpr]]:
|
|||||||
|
|
||||||
|
|
||||||
class FrameManager:
|
class FrameManager:
|
||||||
def __init__(self, types: TypesRegistry) -> None:
|
def __init__(self, typer: PythonTyper) -> None:
|
||||||
self.types: TypesRegistry = types
|
self.typer: PythonTyper = typer
|
||||||
|
self.method_resolver: MethodRegistry = MethodRegistry(self.typer)
|
||||||
|
|
||||||
def assign(
|
def assign(
|
||||||
self,
|
self,
|
||||||
@@ -131,3 +136,19 @@ class FrameManager:
|
|||||||
cls, frame: DataFrameType, names: list[str]
|
cls, frame: DataFrameType, names: list[str]
|
||||||
) -> list[Optional[ColumnType]]:
|
) -> list[Optional[ColumnType]]:
|
||||||
return [cls._get_column(frame, name) for name in names]
|
return [cls._get_column(frame, name) for name in names]
|
||||||
|
|
||||||
|
def call(
|
||||||
|
self,
|
||||||
|
method: str,
|
||||||
|
location: Location,
|
||||||
|
frame: DataFrameType,
|
||||||
|
positional: list[TypedExpr],
|
||||||
|
keywords: dict[str, TypedExpr],
|
||||||
|
) -> Type:
|
||||||
|
call: Call = Call(
|
||||||
|
location=location,
|
||||||
|
frame=frame,
|
||||||
|
positional=positional,
|
||||||
|
keywords=keywords,
|
||||||
|
)
|
||||||
|
return self.method_resolver.call(method, call)
|
||||||
|
|||||||
+19
-5
@@ -75,7 +75,7 @@ class PythonTyper(
|
|||||||
self.logger: logging.Logger = logging.getLogger("PythonTyper")
|
self.logger: logging.Logger = logging.getLogger("PythonTyper")
|
||||||
self.reporter: FileReporter = reporter.for_file(None)
|
self.reporter: FileReporter = reporter.for_file(None)
|
||||||
self.types: TypesRegistry = types
|
self.types: TypesRegistry = types
|
||||||
self.frame_mgr: FrameManager = FrameManager(self.types)
|
self.frame_mgr: FrameManager = FrameManager(self)
|
||||||
self.global_env: Environment = Preamble(self.types)
|
self.global_env: Environment = Preamble(self.types)
|
||||||
self.env: Environment = self.global_env
|
self.env: Environment = self.global_env
|
||||||
self.locals: dict[p.Expr, int] = {}
|
self.locals: dict[p.Expr, int] = {}
|
||||||
@@ -515,13 +515,27 @@ class PythonTyper(
|
|||||||
case p.VariableExpr(name="TypeVar"):
|
case p.VariableExpr(name="TypeVar"):
|
||||||
return self.define_typevar(expr) or UnknownType()
|
return self.define_typevar(expr) or UnknownType()
|
||||||
|
|
||||||
callee: Type = self.type_of(expr.callee)
|
|
||||||
positional: list[TypedExpr] = [
|
positional: list[TypedExpr] = [
|
||||||
(arg, self.type_of(arg)) for arg in expr.arguments
|
(arg, self.type_of(arg)) for arg in expr.arguments
|
||||||
]
|
]
|
||||||
keywords: dict[str, TypedExpr] = {
|
keywords: dict[str, TypedExpr] = {
|
||||||
name: (arg, self.type_of(arg)) for name, arg in expr.keywords.items()
|
name: (arg, self.type_of(arg)) for name, arg in expr.keywords.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
match expr.callee:
|
||||||
|
case p.GetExpr(object=obj, name=method):
|
||||||
|
obj_type: Type = self.type_of(obj)
|
||||||
|
unfolded: Type = unfold_type(obj_type)
|
||||||
|
if isinstance(unfolded, DataFrameType):
|
||||||
|
return self.frame_mgr.call(
|
||||||
|
method,
|
||||||
|
expr.location,
|
||||||
|
unfolded,
|
||||||
|
positional,
|
||||||
|
keywords,
|
||||||
|
)
|
||||||
|
|
||||||
|
callee: Type = self.type_of(expr.callee)
|
||||||
return (
|
return (
|
||||||
self._get_call_result(
|
self._get_call_result(
|
||||||
location=expr.location,
|
location=expr.location,
|
||||||
@@ -626,7 +640,7 @@ class PythonTyper(
|
|||||||
return self.types.apply_generic(list_type, [item_type])
|
return self.types.apply_generic(list_type, [item_type])
|
||||||
self.reporter.error(
|
self.reporter.error(
|
||||||
expr.location,
|
expr.location,
|
||||||
f"Heterogeneous list items: {item_types}",
|
f"Heterogeneous list items: [{', '.join(map(str, item_types))}]",
|
||||||
)
|
)
|
||||||
return self.types.apply_generic(list_type, [UnknownType()])
|
return self.types.apply_generic(list_type, [UnknownType()])
|
||||||
|
|
||||||
@@ -658,7 +672,7 @@ class PythonTyper(
|
|||||||
else:
|
else:
|
||||||
self.reporter.error(
|
self.reporter.error(
|
||||||
expr.location,
|
expr.location,
|
||||||
f"Heterogeneous dict keys: {key_types}",
|
f"Heterogeneous dict keys: [{', '.join(map(str, key_types))}]",
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(value_types) == 1:
|
if len(value_types) == 1:
|
||||||
@@ -666,7 +680,7 @@ class PythonTyper(
|
|||||||
else:
|
else:
|
||||||
self.reporter.error(
|
self.reporter.error(
|
||||||
expr.location,
|
expr.location,
|
||||||
f"Heterogeneous dict values: {value_types}",
|
f"Heterogeneous dict values: [{', '.join(map(str, value_types))}]",
|
||||||
)
|
)
|
||||||
return self.types.apply_generic(dict_type, [key_type, value_type])
|
return self.types.apply_generic(dict_type, [key_type, value_type])
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ from midas.checker.types import (
|
|||||||
AliasType,
|
AliasType,
|
||||||
AppliedType,
|
AppliedType,
|
||||||
BaseType,
|
BaseType,
|
||||||
|
ColumnType,
|
||||||
ComplexType,
|
ComplexType,
|
||||||
ConstraintType,
|
ConstraintType,
|
||||||
|
DataFrameType,
|
||||||
ExtensionType,
|
ExtensionType,
|
||||||
Function,
|
Function,
|
||||||
GenericType,
|
GenericType,
|
||||||
@@ -157,6 +159,24 @@ class TypesRegistry:
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
case (DataFrameType(columns=columns1), DataFrameType(columns=columns2)):
|
||||||
|
# TODO: check order?
|
||||||
|
by_name1: dict[str, DataFrameType.Column] = {
|
||||||
|
col.name: col for col in columns1 if col.name is not None
|
||||||
|
}
|
||||||
|
for col2 in columns2:
|
||||||
|
if col2.name not in by_name1:
|
||||||
|
return False
|
||||||
|
if not self.is_subtype(by_name1[col2.name].type, col2.type):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
case (ColumnType(type=inner1), ColumnType(type=inner2)):
|
||||||
|
# TODO: invariant, replace ColumnType with simple GenericType
|
||||||
|
if not self.are_equivalent(inner1, inner2):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
case (Function(), Function()):
|
case (Function(), Function()):
|
||||||
return self.is_func_subtype(type1, type2)
|
return self.is_func_subtype(type1, type2)
|
||||||
|
|
||||||
@@ -187,6 +207,9 @@ class TypesRegistry:
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def are_equivalent(self, type1: Type, type2: Type) -> bool:
|
||||||
|
return self.is_subtype(type1, type2) and self.is_subtype(type2, type1)
|
||||||
|
|
||||||
# TODO: verify the logic in here
|
# TODO: verify the logic in here
|
||||||
def is_func_subtype(self, func1: Function, func2: Function) -> bool:
|
def is_func_subtype(self, func1: Function, func2: Function) -> bool:
|
||||||
"""Check whether a function is a subtype of another
|
"""Check whether a function is a subtype of another
|
||||||
|
|||||||
+25
-1
@@ -68,7 +68,7 @@ class DiagnosticPrinter:
|
|||||||
|
|
||||||
loc: Location = diagnostic.location
|
loc: Location = diagnostic.location
|
||||||
if loc.lineno != loc.end_lineno:
|
if loc.lineno != loc.end_lineno:
|
||||||
print(diagnostic)
|
self.print_multiline(lines, diagnostic, indent)
|
||||||
return
|
return
|
||||||
|
|
||||||
start_offset: int = loc.col_offset
|
start_offset: int = loc.col_offset
|
||||||
@@ -95,3 +95,27 @@ class DiagnosticPrinter:
|
|||||||
print(indent_str + before + subject + after)
|
print(indent_str + before + subject + after)
|
||||||
print(indent_str + cursor)
|
print(indent_str + cursor)
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
def print_multiline(
|
||||||
|
self, all_lines: list[str], diagnostic: Diagnostic, indent: int = 4
|
||||||
|
):
|
||||||
|
loc: Location = diagnostic.location
|
||||||
|
lines: list[str] = all_lines[loc.lineno - 1 : loc.end_lineno]
|
||||||
|
|
||||||
|
start_offset: int = loc.col_offset
|
||||||
|
end_offset: int = loc.end_col_offset or (start_offset + 1)
|
||||||
|
|
||||||
|
indent_str: str = " " * indent
|
||||||
|
color: int = self.COLORS.get(diagnostic.type, Ansi.WHITE)
|
||||||
|
res: str = indent_str + lines[0][:start_offset]
|
||||||
|
res += Ansi.FG(color) + lines[0][start_offset:]
|
||||||
|
for line in lines[1:-1]:
|
||||||
|
res += "\n" + indent_str + line
|
||||||
|
res += "\n" + indent_str + lines[-1][:end_offset]
|
||||||
|
res += Ansi.RESET + lines[-1][end_offset:]
|
||||||
|
|
||||||
|
print(diagnostic.location_str + ":")
|
||||||
|
print(res)
|
||||||
|
print()
|
||||||
|
print(Ansi.FG(color) + diagnostic.message + Ansi.RESET)
|
||||||
|
print()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from typing import Generic, TypeVar
|
||||||
from typing import cast as typing_cast
|
from typing import cast as typing_cast
|
||||||
|
|
||||||
cast = typing_cast
|
cast = typing_cast
|
||||||
@@ -32,3 +33,20 @@ This operation is unsound, use at your own risk!
|
|||||||
|
|
||||||
_**Internal Python documentation**_
|
_**Internal Python documentation**_
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
class Frame(Generic[T]):
|
||||||
|
"""A `Frame` is the abstract type implemented by `DataFrame`
|
||||||
|
|
||||||
|
A frame contains any number of named columns (see :class:`Column`)
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Column(Generic[T]):
|
||||||
|
"""A `Column` is the abstract type implemented by `Series`
|
||||||
|
|
||||||
|
A column contains a any number of values of the same type
|
||||||
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user