feat(checker): delegate element operation to inner type

delegate element-wise binary operation on columns to their inner types
This commit is contained in:
2026-07-03 00:05:40 +02:00
parent 1bc4c704c3
commit be2fd4c837
3 changed files with 61 additions and 43 deletions

View File

@@ -35,11 +35,19 @@ class Call:
class ColumnMethodRegistry(MethodRegistry[Call]): class ColumnMethodRegistry(MethodRegistry[Call]):
@method("add", "__add__") def _element_binary_op(self, call: Call, method: str) -> ColumnType:
def add(self, call: Call) -> Type: """Compute the result of an element-wise binary operation
# TODO: support add with scalar
# TODO: check operation exists on inner column types
This function delegates to the inner types for computing the resulting
type.
Args:
call (Call): the call that triggered this resolution
method (str): the method name
Returns:
ColumnType: the resulting column type
"""
column2: Optional[ColumnType] = None column2: Optional[ColumnType] = None
col_type1: Type = call.column.type col_type1: Type = call.column.type
@@ -50,8 +58,20 @@ class ColumnMethodRegistry(MethodRegistry[Call]):
if isinstance(unfolded_other, ColumnType): if isinstance(unfolded_other, ColumnType):
column2 = unfolded_other column2 = unfolded_other
col_type2: Type = column2.type col_type2: Type = column2.type
if self.types.are_equivalent(col_type2, col_type1):
new_column = ColumnType(type=col_type1) new_inner_type = self.typer.result_of_binary_op(
location=call.location,
expr=call.call_expr,
left=(call.column_expr, col_type1),
right=(call.positional[0][0], col_type2),
method=method,
)
new_column = ColumnType(type=new_inner_type)
return new_column
@method("add", "__add__")
def add(self, call: Call) -> Type:
# TODO: support add with scalar
# Build signature with new column type and generic operand # Build signature with new column type and generic operand
param_type: TypeVar = TypeVar(name="T", bound=None) param_type: TypeVar = TypeVar(name="T", bound=None)
@@ -67,7 +87,7 @@ class ColumnMethodRegistry(MethodRegistry[Call]):
required=True, required=True,
), ),
], ],
returns=new_column, returns=self._element_binary_op(call, "__add__"),
), ),
) )

View File

@@ -21,7 +21,7 @@ from midas.checker.types import (
) )
if TYPE_CHECKING: if TYPE_CHECKING:
from midas.checker.python import TypedExpr, UndefinedMethodException from midas.checker.python import TypedExpr
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
@@ -60,28 +60,17 @@ class FrameMethodRegistry(MethodRegistry[Call]):
If the operation is invalid / doesn't exist, If the operation is invalid / doesn't exist,
`ColumnType(type=UnknownType())` is returned `ColumnType(type=UnknownType())` is returned
""" """
unknown: ColumnType = ColumnType(type=UnknownType())
result: Type = unknown result: Type = self.typer.result_of_binary_op(
try: location=call.location,
result = ( expr=call.call_expr,
self.typer.call_method( left=(call.frame_expr, column1),
location=call.location, right=(call.positional[0][0], column2),
call_expr=call.call_expr, method=method,
obj=(call.frame_expr, column1), )
method_name=method,
positional=[(call.positional[0][0], column2)],
keywords={},
)
or unknown
)
except UndefinedMethodException:
self.reporter.error(
call.location,
f"Undefined operation {method} between {column1} and {column2}",
)
if not isinstance(result, ColumnType): if not isinstance(result, ColumnType):
result = unknown return ColumnType(type=UnknownType())
return result return result
def _element_binary_op(self, call: Call, method: str) -> DataFrameType: def _element_binary_op(self, call: Call, method: str) -> DataFrameType:
@@ -105,8 +94,8 @@ class FrameMethodRegistry(MethodRegistry[Call]):
frame2: Optional[DataFrameType] = None frame2: Optional[DataFrameType] = None
# Get map of operand's columns by name, if there is at least 1 operand, which is a dataframe # Get map of operand's columns by name, if there is at least 1 operand, which is a dataframe
if len(call.positional) != 0: if len(call.positional) != 0:
other: Type = call.positional[0][1] operand: TypedExpr = call.positional[0]
unfolded_other: Type = unfold_type(other) unfolded_other: Type = unfold_type(operand[1])
if isinstance(unfolded_other, DataFrameType): if isinstance(unfolded_other, DataFrameType):
frame2 = unfolded_other frame2 = unfolded_other
by_name = { by_name = {

View File

@@ -543,8 +543,14 @@ class PythonTyper(
) )
return UnknownType() return UnknownType()
return self._visit_binary_expr( left: Type = self.type_of(expr.left)
expr.location, expr, expr.left, expr.right, method right: Type = self.type_of(expr.right)
return self.result_of_binary_op(
expr.location,
expr,
(expr.left, left),
(expr.right, right),
method,
) )
def visit_compare_expr(self, expr: p.CompareExpr) -> Type: def visit_compare_expr(self, expr: p.CompareExpr) -> Type:
@@ -556,35 +562,38 @@ class PythonTyper(
) )
return UnknownType() return UnknownType()
return self._visit_binary_expr( left: Type = self.type_of(expr.left)
expr.location, expr, expr.left, expr.right, method right: Type = self.type_of(expr.right)
return self.result_of_binary_op(
expr.location,
expr,
(expr.left, left),
(expr.right, right),
method,
) )
def _visit_binary_expr( def result_of_binary_op(
self, self,
location: Location, location: Location,
expr: p.Expr, expr: p.Expr,
left_expr: p.Expr, left: TypedExpr,
right_expr: p.Expr, right: TypedExpr,
method: str, method: str,
) -> Type: ) -> Type:
left: Type = self.type_of(left_expr)
right: Type = self.type_of(right_expr)
result: Optional[Type] result: Optional[Type]
try: try:
result = self.call_method( result = self.call_method(
location=location, location=location,
call_expr=expr, call_expr=expr,
obj=(left_expr, left), obj=left,
method_name=method, method_name=method,
positional=[(right_expr, right)], positional=[right],
keywords={}, keywords={},
) )
except UndefinedMethodException: except UndefinedMethodException:
self.reporter.error( self.reporter.error(
location, location,
f"Undefined operation {method} between {left} and {right}", f"Undefined operation {method} between {left[1]} and {right[1]}",
) )
return UnknownType() return UnknownType()