diff --git a/midas/checker/registry.py b/midas/checker/registry.py index 3ce265e..d85456e 100644 --- a/midas/checker/registry.py +++ b/midas/checker/registry.py @@ -1,6 +1,6 @@ import logging from dataclasses import dataclass -from typing import Optional +from typing import Optional, TypeAlias from midas.ast.midas import MemberKind from midas.checker.builtins import BUILTIN_SUBTYPES @@ -24,6 +24,8 @@ from midas.checker.types import ( substitute_typevars, ) +Match: TypeAlias = tuple[Function.Parameter, Function.Parameter] + @dataclass class Member: @@ -259,7 +261,6 @@ class TypesRegistry: """ return self.is_subtype(type1, type2) and self.is_subtype(type2, type1) - # TODO: verify the logic in here def is_func_subtype(self, func1: Function, func2: Function) -> bool: """Check whether a function is a subtype of another @@ -270,14 +271,24 @@ class TypesRegistry: Returns: bool: whether `func1` is a subtype of `func2` """ + + # Let func1 = (S1, R1) where S1 = (P1, M1, K1) + # Let func2 = (S2, R2) where S2 = (P2, M2, K2) + # We want to check that func1 <: func2 + # i.e. R1 <: R2 and S2 <: S1 + + # R1 <: R2 if not self.is_subtype(func1.returns, func2.returns): return False + # Extract P1, M1, K1 pos1: list[Function.Parameter] = func1.params.pos mixed1: list[Function.Parameter] = func1.params.mixed kw1: dict[str, Function.Parameter] = { param.name: param for param in func1.params.kw } + + # Extract P2, M2, K2 pos2: list[Function.Parameter] = func2.params.pos mixed2: list[Function.Parameter] = func2.params.mixed kw2: dict[str, Function.Parameter] = { @@ -285,89 +296,118 @@ class TypesRegistry: } mixed_by_pos: dict[int, Function.Parameter] = { - param.pos: param for param in mixed2 + param.pos: param for param in mixed1 } mixed_by_name: dict[str, Function.Parameter] = { - param.name: param for param in mixed2 + param.name: param for param in mixed1 } - def is_arg_subtype(sub: Function.Parameter, sup: Function.Parameter) -> bool: - if not self.is_subtype(sub.type, sup.type): - return False - if not sup.required and sub.required: - return False - return True + matches: list[Match] = [] - for param1 in pos1: - param2: Function.Parameter - if param1.pos < len(pos2): - param2 = pos2[param1.pos] - elif param1.pos in mixed_by_pos: - param2 = mixed_by_pos[param1.pos] - elif not param1.required: - continue - else: - return False - if not is_arg_subtype(param2, param1): - return False - - for name, param1 in kw1.items(): - param2: Function.Parameter - if name in kw2: - param2 = kw2[name] - elif name in mixed_by_name: - param2 = mixed_by_name[name] - elif not param1.required: - continue - else: - return False - if not is_arg_subtype(param2, param1): - return False - - for param1 in mixed1: - pos_param2: Optional[Function.Parameter] = None - kw_param2: Optional[Function.Parameter] = None - if param1.name in kw2: - kw_param2 = kw2[param1.name] - elif param1.name in mixed_by_name: - kw_param2 = mixed_by_name[param1.name] - if param1.pos < len(pos2): - pos_param2 = pos2[param1.pos] - elif param1.pos in mixed_by_pos: - pos_param2 = mixed_by_pos[param1.pos] - - # No match in func2 and arg is required - if pos_param2 is None and kw_param2 is None and param1.required: - return False - - # Matching keyword argument - if kw_param2 is not None and not is_arg_subtype(kw_param2, param1): - return False - - # Matching positional argument - if pos_param2 is not None and not is_arg_subtype(pos_param2, param1): - return False - - mixed_positions: set[int] = {param.pos for param in mixed1} - mixed_names: set[str] = {param.name for param in mixed1} + # Each parameter at position i in P2 must be valid at position i in S1 + # either as a positional-only parameter in P1 + # or a mixed parameter in M1 for param2 in pos2: - if not param2.required: - continue - if param2.pos >= len(pos1) and param2.pos not in mixed_positions: + param1: Function.Parameter + + # In P1 + if param2.pos < len(pos1): + param1 = pos1[param2.pos] + + # In M1 + elif param2.pos in mixed_by_pos: + param1 = mixed_by_pos[param2.pos] + + else: return False + # not req(p2) => not req(p1) + if not param2.required and param1.required: + return False + matches.append((param1, param2)) + + # Each parameter named p in K2 must be valid with name p in S1 + # either as a keyword-only parameter in K1 + # or a mixed parameter in M1 for name, param2 in kw2.items(): - if not param2.required: - continue - if name not in kw1 and name not in mixed_names: + param1: Function.Parameter + + # In K1 + if name in kw1: + param1 = kw1[name] + + # in M1 + elif name in mixed_by_name: + param1 = mixed_by_name[name] + else: return False + # not req(p2) => not req(p1) + if not param2.required and param1.required: + return False + matches.append((param1, param2)) + + # Each parameter named p at position i in M2 must be valid with name p + # in S1 *and* at position i in S1 + # either as a single mixed parameter in M1 + # or split into a positional parameter in P1/M1 + # and a keyword parameter in K1/M1 for param2 in mixed2: - if param2.required: - continue - pos_match: bool = param2.pos < len(pos1) or param2.pos in mixed_positions - kw_match: bool = param2.name in kw1 or param2.name in mixed_names - if not pos_match or not kw_match: + pos_param1: Optional[Function.Parameter] = None + kw_param1: Optional[Function.Parameter] = None + + # By name in K1 + if param2.name in kw1: + kw_param1 = kw1[param2.name] + + # By name in M1 + elif param2.name in mixed_by_name: + kw_param1 = mixed_by_name[param2.name] + + # By pos in P1 + if param2.pos < len(pos1): + pos_param1 = pos1[param2.pos] + + # By pos in M1 + elif param2.pos in mixed_by_pos: + pos_param1 = mixed_by_pos[param2.pos] + + # Not fully covered + if pos_param1 is None or kw_param1 is None: + return False + + # Covered by unique mixed parameter in M1 + if pos_param1 == kw_param1: + param1: Function.Parameter = pos_param1 + # not req(p2) => not req(p1) + if not param2.required and param1.required: + return False + + matches.append((param1, param2)) + + else: + # not req(p1) + if pos_param1.required or kw_param1.required: + return False + + matches.append((pos_param1, param2)) + matches.append((kw_param1, param2)) + + def is_matched(param: Function.Parameter) -> bool: + for p1, _ in matches: + if p1 == param: + return True + return False + + all_params1: list[Function.Parameter] = pos1 + mixed1 + list(kw1.values()) + + for param1 in all_params1: + # No new required parameters + if not is_matched(param1) and param1.required: + return False + + for param1, param2 in matches: + if not self.is_subtype(param2.type, param1.type): return False return True