feat(checker): add diagnostics

This commit is contained in:
2026-05-28 18:19:02 +02:00
parent 218b0c5b78
commit 7515716864
4 changed files with 95 additions and 15 deletions

View File

@@ -4,9 +4,11 @@ from typing import Optional
import midas.ast.midas as m
import midas.ast.python as p
from midas.ast.location import Location
from midas.checker.diagnostic import Diagnostic, DiagnosticType
from midas.checker.environment import Environment
from midas.checker.operators import OPERATOR_METHODS
from midas.checker.types import BaseType, Type, UnknownType
from midas.checker.types import Type, UnknownType
from midas.lexer.midas import MidasLexer
from midas.lexer.token import Token
from midas.parser.midas import MidasParser
@@ -18,22 +20,56 @@ class Checker(
p.Expr.Visitor[Type],
p.MidasType.Visitor[Type],
):
def __init__(self, locals: dict[p.Expr, int], base_dir: Path):
def __init__(self, locals: dict[p.Expr, int], file_path: Path):
self.logger: logging.Logger = logging.getLogger("Checker")
self.base_dir: Path = base_dir
self.file_path: Path = file_path
self.ctx: MidasResolver = MidasResolver()
self.global_env: Environment = Environment()
self.env: Environment = self.global_env
self.locals: dict[p.Expr, int] = locals
self.diagnostics: list[Diagnostic] = []
def diagnostic(self, type: DiagnosticType, location: Location, message: str):
self.diagnostics.append(
Diagnostic(
file_path=self.file_path,
location=location,
type=type,
message=message,
)
)
def error(self, location: Location, message: str):
self.diagnostic(
type=DiagnosticType.ERROR,
location=location,
message=message,
)
def warning(self, location: Location, message: str):
self.diagnostic(
type=DiagnosticType.WARNING,
location=location,
message=message,
)
def info(self, location: Location, message: str):
self.diagnostic(
type=DiagnosticType.INFO,
location=location,
message=message,
)
def evaluate(self, expr: p.Expr) -> Type:
return expr.accept(self)
def check(self, statements: list[p.Stmt]) -> None:
def check(self, statements: list[p.Stmt]) -> list[Diagnostic]:
self.diagnostics = []
for stmt in statements:
stmt.accept(self)
self.logger.debug(f"Final environment: {self.env.flat_dict()}")
return self.diagnostics
def look_up_variable(self, name: str, expr: p.Expr) -> Optional[Type]:
distance: Optional[int] = self.locals.get(expr)
@@ -57,7 +93,7 @@ class Checker(
def import_midas(self, path: Path) -> None:
self.logger.debug(f"Importing type definitions from {path}")
path = (self.base_dir / path).resolve()
path = (self.file_path.parent / path).resolve()
lexer: MidasLexer = MidasLexer(path.read_text())
tokens: list[Token] = lexer.process()
parser: MidasParser = MidasParser(tokens)
@@ -81,6 +117,7 @@ class Checker(
for target in stmt.targets:
if not isinstance(target, p.VariableExpr):
self.logger.warning(f"Unsupported assignment to {target}")
self.warning(target.location, f"Unsupported assignment to {target}")
continue
name: str = target.name
var_type: Optional[Type] = self.look_up_variable(name, target)
@@ -90,19 +127,27 @@ class Checker(
else:
# TODO: implement real comparison method
if var_type != value:
raise ValueError(
f"Cannot assign {value} to {name} of type {var_type}"
self.error(
stmt.location,
f"Cannot assign {value} to {name} of type {var_type}",
)
def visit_binary_expr(self, expr: p.BinaryExpr) -> Type:
method: Optional[str] = OPERATOR_METHODS.get(expr.operator.__class__)
if method is None:
self.logger.warning(f"Unsupported operator {expr.operator}")
self.warning(expr.location, f"Unsupported operator {expr.operator}")
return UnknownType()
left: Type = self.evaluate(expr.left)
right: Type = self.evaluate(expr.right)
result: Type = self.ctx.get_operation_result(left, method, right)
result: Optional[Type] = self.ctx.get_operation_result(left, method, right)
if result is None:
self.error(
expr.location,
f"Undefined operation {method} between {left} and {right}",
)
return UnknownType()
return result
def visit_compare_expr(self, expr: p.CompareExpr) -> Type: ...
@@ -133,6 +178,7 @@ class Checker(
case str():
return self.ctx.get_type("str")
case _:
self.warning(expr.location, f"Unknown literal {expr}")
return UnknownType()
def visit_variable_expr(self, expr: p.VariableExpr) -> Type:

View File

@@ -0,0 +1,33 @@
from dataclasses import dataclass
from enum import StrEnum
from pathlib import Path
from typing import Optional
from midas.ast.location import Location
class DiagnosticType(StrEnum):
ERROR = "Error"
WARNING = "Warning"
INFO = "Info"
@dataclass(frozen=True)
class Diagnostic:
file_path: Path
location: Location
type: DiagnosticType
message: str
def __str__(self) -> str:
start_loc: str = f"L{self.location.lineno}:{self.location.col_offset+1}"
end_loc: Optional[str] = ""
if (
self.location.end_lineno is not None
and self.location.end_col_offset is not None
):
end_loc = f"L{self.location.end_lineno}:{self.location.end_col_offset+1}"
loc: str = (
f"at {start_loc}" if end_loc is None else f"from {start_loc} to {end_loc}"
)
return f"{self.type} in {self.file_path} {loc}: {self.message}"

View File

@@ -11,6 +11,7 @@ import midas.ast.python as p
from midas.ast.location import Location
from midas.ast.printer import PythonAstPrinter
from midas.checker.checker import Checker
from midas.checker.diagnostic import Diagnostic
from midas.cli.highlighter import Highlighter, MidasHighlighter, PythonHighlighter
from midas.lexer.midas import MidasLexer
from midas.lexer.token import Token, TokenType
@@ -34,8 +35,10 @@ def compile(file: TextIO):
stmts: list[p.Stmt] = parser.parse_module(tree)
resolver = Resolver()
resolver.resolve(*stmts)
checker = Checker(resolver.locals, base_dir=Path(file.name).resolve().parent)
checker.check(stmts)
checker = Checker(resolver.locals, file_path=Path(file.name).resolve())
diagnostics: list[Diagnostic] = checker.check(stmts)
for diagnostic in diagnostics:
print(diagnostic)
@midas.group()

View File

@@ -17,13 +17,11 @@ class MidasResolver(m.Stmt.Visitor[None], m.Expr.Visitor[Type]):
raise NameError(f"Undefined type {name}")
return type
def get_operation_result(self, left: Type, operator: str, right: Type) -> Type:
def get_operation_result(
self, left: Type, operator: str, right: Type
) -> Optional[Type]:
operation: tuple[Type, str, Type] = (left, operator, right)
result: Optional[Type] = self._operations.get(operation)
if result is None:
raise ValueError(
f"Undefined operation {operator} between {left} and {right}"
)
return result
def _define_builtin(self):