4 Commits
Author SHA1 Message Date
HEL a9a4d6d4c1 fix(checker): allow calling AppliedType and UnknownType 2026-06-16 14:04:20 +02:00
HEL 60144f8671 fix(cli): improve dump-registry command output 2026-06-16 14:03:13 +02:00
HEL bbede34a84 feat(checker): add environment preamble
this adds some builtin functions such as the builtin type constructors
2026-06-16 14:02:45 +02:00
HEL 16d6e1b603 fix(checker): allow inferred return to by subtype of hint 2026-06-16 12:55:05 +02:00
4 changed files with 156 additions and 12 deletions
+121
View File
@@ -0,0 +1,121 @@
from dataclasses import dataclass
from midas.checker.environment import Environment
from midas.checker.registry import TypesRegistry
from midas.checker.types import Function, GenericType, TopType, Type, TypeVar, UnitType
@dataclass(frozen=True)
class Param:
name: str
type: Type
required: bool = True
class Preamble(Environment):
def __init__(self, types: TypesRegistry) -> None:
super().__init__()
self._types: TypesRegistry = types
self._def_type_constructor("object")
self._def_type_constructor("float")
self._def_type_constructor("int")
self._def_type_constructor("bool")
self._def_type_constructor("str")
self._def_function(
name="list",
pos=[Param("object", TopType())],
returns=self._list_of(TopType()),
)
# TODO: use sink
self._def_function(
name="print",
pos=[Param("object", TopType())],
returns=UnitType(),
)
map_in = TypeVar(name="T", bound=None)
map_out = TypeVar(name="U", bound=None)
mapper = self._make_function(
name="MapTransform",
pos=[Param("v", map_in)],
returns=map_out,
)
self._def_function(
name="map",
pos=[
Param("transform", mapper),
Param(
"iterable",
self._list_of(map_in), # TODO: replace with Iterable[T]
),
],
returns=self._list_of(map_out), # TODO: replace with Iterable[U]
)
def _list_of(self, item_type: Type) -> Type:
return self._types.apply_generic(self._types.get_type("list"), [item_type])
def _def_type_constructor(self, name: str):
# TODO: more specific arg types
self._def_function(
name=name,
pos=[Param("object", TopType())],
returns=self._types.get_type(name),
)
def _make_function(
self,
*,
name: str,
pos: list[Param] = [],
mixed: list[Param] = [],
kw: list[Param] = [],
returns: Type = UnitType(),
type_vars: list[TypeVar] = [],
) -> Type:
def map_args(params: list[Param], offset: int) -> list[Function.Argument]:
return [
Function.Argument(
pos=i + offset,
name=param.name,
type=param.type,
required=param.required,
)
for i, param in enumerate(params)
]
function = Function(
pos_args=map_args(pos, 0),
args=map_args(mixed, len(pos)),
kw_args=map_args(kw, len(pos) + len(mixed)),
returns=returns,
)
if len(type_vars) != 0:
function = GenericType(
name=name,
params=type_vars,
body=function,
)
return function
def _def_function(
self,
*,
name: str,
pos: list[Param] = [],
mixed: list[Param] = [],
kw: list[Param] = [],
returns: Type = UnitType(),
type_vars: list[TypeVar] = [],
):
function: Type = self._make_function(
name=name,
pos=pos,
mixed=mixed,
kw=kw,
returns=returns,
type_vars=type_vars,
)
self.define(name, function)
+13 -2
View File
@@ -7,10 +7,12 @@ import midas.ast.python as p
from midas.ast.location import Location from midas.ast.location import Location
from midas.checker.environment import Environment from midas.checker.environment import Environment
from midas.checker.operators import COMPARATOR_METHODS, OPERATOR_METHODS, UNARY_METHODS from midas.checker.operators import COMPARATOR_METHODS, OPERATOR_METHODS, UNARY_METHODS
from midas.checker.preamble import Preamble
from midas.checker.registry import TypesRegistry from midas.checker.registry import TypesRegistry
from midas.checker.reporter import FileReporter, Reporter from midas.checker.reporter import FileReporter, Reporter
from midas.checker.resolver import Resolver from midas.checker.resolver import Resolver
from midas.checker.types import ( from midas.checker.types import (
AppliedType,
Function, Function,
OverloadedFunction, OverloadedFunction,
Type, Type,
@@ -56,7 +58,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.global_env: Environment = Environment() 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] = {}
self.judgements: list[tuple[p.Expr, Type]] = [] self.judgements: list[tuple[p.Expr, Type]] = []
@@ -252,7 +254,7 @@ class PythonTyper(
if returns_hint is not None: if returns_hint is not None:
assert stmt.returns is not None assert stmt.returns is not None
returns = returns_hint returns = returns_hint
if returns != inferred_return: if not self.is_subtype(inferred_return, returns):
self.reporter.error( self.reporter.error(
stmt.returns.location, stmt.returns.location,
f"Return type mismatch, annotated {returns} but returns {inferred_return}", f"Return type mismatch, annotated {returns} but returns {inferred_return}",
@@ -643,6 +645,15 @@ class PythonTyper(
if function is None: if function is None:
return None return None
return function.returns return function.returns
case AppliedType(body=body):
return self._get_call_result(
location, body, positional, keywords, report_errors
)
case UnknownType():
return UnknownType()
case _: case _:
if report_errors: if report_errors:
self.reporter.error(location, f"{callee} is not callable") self.reporter.error(location, f"{callee} is not callable")
+4 -6
View File
@@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass, field
from typing import Optional from typing import Optional
@@ -41,23 +41,21 @@ class UnitType:
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
class Function: class Function:
pos_args: list[Argument] pos_args: list[Argument] = field(default_factory=list)
args: list[Argument] args: list[Argument] = field(default_factory=list)
kw_args: list[Argument] kw_args: list[Argument] = field(default_factory=list)
returns: Type returns: Type
def __str__(self) -> str: def __str__(self) -> str:
args: list[str] = [] args: list[str] = []
if len(self.pos_args) != 0: if len(self.pos_args) != 0:
args += list(map(str, self.pos_args)) args += list(map(str, self.pos_args))
if len(self.args) + len(self.kw_args) != 0:
args.append("/") args.append("/")
if len(self.args) != 0: if len(self.args) != 0:
args += list(map(str, self.args)) args += list(map(str, self.args))
if len(self.kw_args) != 0: if len(self.kw_args) != 0:
if len(args) != 0:
args.append("*") args.append("*")
args += list(map(str, self.kw_args)) args += list(map(str, self.kw_args))
+16 -2
View File
@@ -9,7 +9,21 @@ from typing import TextIO
import click import click
from midas.checker.checker import TypeChecker from midas.checker.checker import TypeChecker
from midas.checker.types import Type from midas.checker.types import AliasType, AppliedType, BaseType, GenericType, Type
def base_type(type: Type) -> Type:
match type:
case BaseType():
return type
case AliasType(type=base):
return base
case AppliedType(body=body):
return body
case GenericType(body=body):
return body
case _:
return type
@click.command(help="Dump types registry") @click.command(help="Dump types registry")
@@ -23,7 +37,7 @@ def dump_registry(
for name, type in checker.types._types.items(): for name, type in checker.types._types.items():
members: dict[str, Type] = checker.types._members.get(name, {}) members: dict[str, Type] = checker.types._members.get(name, {})
print(f"{name} = {type}") print(f"{name} = {base_type(type)}")
if len(members) != 0: if len(members) != 0:
print(" " * 4 + "Members:") print(" " * 4 + "Members:")
for member_name, member_type in members.items(): for member_name, member_type in members.items():