Compare commits
5
Commits
bccd75317e
...
d278dc5f5b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d278dc5f5b
|
||
|
|
59e73f0fd9
|
||
|
|
3e0dc60283
|
||
|
|
c24eb5125e
|
||
|
|
25bd895dde
|
+42
-14
@@ -15,6 +15,7 @@ from midas.checker.types import (
|
||||
BaseType,
|
||||
ComplexType,
|
||||
Function,
|
||||
Operation,
|
||||
Type,
|
||||
UnitType,
|
||||
UnknownType,
|
||||
@@ -177,13 +178,6 @@ class Checker(
|
||||
stmts: list[m.Stmt] = parser.parse()
|
||||
self.ctx.resolve(stmts)
|
||||
|
||||
def unfold_type(self, type: Type) -> Type:
|
||||
match type:
|
||||
case AliasType(type=ref_type):
|
||||
return self.unfold_type(ref_type)
|
||||
case _:
|
||||
return type
|
||||
|
||||
def is_subtype(self, type1: Type, type2: Type) -> bool:
|
||||
"""Check whether `type1` is a subtype of `type2`
|
||||
|
||||
@@ -197,13 +191,13 @@ class Checker(
|
||||
bool: whether `type1` is a subtype of `type2`
|
||||
"""
|
||||
|
||||
type1 = self.unfold_type(type1)
|
||||
type2 = self.unfold_type(type2)
|
||||
|
||||
if type1 == type2:
|
||||
return True
|
||||
|
||||
match (type1, type2):
|
||||
case (AliasType(type=base1), _):
|
||||
return self.is_subtype(base1, type2)
|
||||
|
||||
case (BaseType(name=name1), BaseType(name=name2)):
|
||||
return name1 in BUILTIN_SUBTYPES.get(name2, set())
|
||||
|
||||
@@ -211,7 +205,7 @@ class Checker(
|
||||
for k, t in props2.items():
|
||||
if k not in props1:
|
||||
return False
|
||||
if self.is_subtype(props1[k], t):
|
||||
if not self.is_subtype(props1[k], t):
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -490,14 +484,48 @@ class Checker(
|
||||
left: Type = self.type_of(expr.left)
|
||||
right: Type = self.type_of(expr.right)
|
||||
|
||||
result: Optional[Type] = self.ctx.get_operation_result(left, method, right)
|
||||
if result is None:
|
||||
operations: list[Operation] = self.ctx.get_operations_by_name(method)
|
||||
valid_operations: list[Operation] = []
|
||||
for op in operations:
|
||||
sig: Operation.CallSignature = op.signature
|
||||
if self.is_subtype(left, sig.left) and self.is_subtype(right, sig.right):
|
||||
valid_operations.append(op)
|
||||
|
||||
if len(valid_operations) == 0:
|
||||
self.error(
|
||||
expr.location,
|
||||
f"Undefined operation {method} between {left} and {right}",
|
||||
)
|
||||
return UnknownType()
|
||||
return result
|
||||
elif len(valid_operations) == 1:
|
||||
self.logger.debug(f"Unique operation {method} between {left} and {right}")
|
||||
return valid_operations[0].result
|
||||
|
||||
for i, op1 in enumerate(valid_operations):
|
||||
sig1: Operation.CallSignature = op1.signature
|
||||
best_match: bool = True
|
||||
for j, op2 in enumerate(valid_operations):
|
||||
if i == j:
|
||||
continue
|
||||
sig2: Operation.CallSignature = op2.signature
|
||||
if not self.is_subtype(sig1.left, sig2.left) or not self.is_subtype(
|
||||
sig1.right, sig2.right
|
||||
):
|
||||
best_match = False
|
||||
break
|
||||
self.logger.debug(f"{op1} is a full overload of {op2}")
|
||||
if best_match:
|
||||
return op1.result
|
||||
|
||||
overloads: list[str] = [
|
||||
f"({op.signature.left} {op.signature.method} {op.signature.right}) -> {op.result}"
|
||||
for op in valid_operations
|
||||
]
|
||||
self.error(
|
||||
expr.location,
|
||||
f"Ambiguous operation {method} between {left} and {right}, multiple matching overloads: {', '.join(overloads)}",
|
||||
)
|
||||
return UnknownType()
|
||||
|
||||
def visit_compare_expr(self, expr: p.CompareExpr) -> Type:
|
||||
method: Optional[str] = COMPARATOR_METHODS.get(expr.operator.__class__)
|
||||
|
||||
@@ -19,7 +19,8 @@ class Diagnostic:
|
||||
type: DiagnosticType
|
||||
message: str
|
||||
|
||||
def __str__(self) -> str:
|
||||
@property
|
||||
def location_str(self) -> str:
|
||||
start_loc: str = f"L{self.location.lineno}:{self.location.col_offset+1}"
|
||||
end_loc: Optional[str] = ""
|
||||
if (
|
||||
@@ -30,4 +31,7 @@ class Diagnostic:
|
||||
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}"
|
||||
return f"{self.type} in {self.file_path} {loc}"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.location_str}: {self.message}"
|
||||
|
||||
@@ -45,4 +45,16 @@ class ComplexType:
|
||||
properties: dict[str, Type]
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class Operation:
|
||||
signature: CallSignature
|
||||
result: Type
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class CallSignature:
|
||||
left: Type
|
||||
method: str
|
||||
right: Type
|
||||
|
||||
|
||||
Type = BaseType | AliasType | UnknownType | UnitType | Function | ComplexType
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
class Ansi:
|
||||
CTRL = "\x1b["
|
||||
RESET = CTRL + "0m"
|
||||
BOLD = CTRL + "1m"
|
||||
DIM = CTRL + "2m"
|
||||
ITALIC = CTRL + "3m"
|
||||
UNDERLINE = CTRL + "4m"
|
||||
|
||||
BLACK = 0
|
||||
RED = 1
|
||||
GREEN = 2
|
||||
YELLOW = 3
|
||||
BLUE = 4
|
||||
MAGENTA = 5
|
||||
CYAN = 6
|
||||
WHITE = 7
|
||||
|
||||
BRIGHT_BLACK = 60
|
||||
BRIGHT_RED = 61
|
||||
BRIGHT_GREEN = 62
|
||||
BRIGHT_YELLOW = 63
|
||||
BRIGHT_BLUE = 64
|
||||
BRIGHT_MAGENTA = 65
|
||||
BRIGHT_CYAN = 66
|
||||
BRIGHT_WHITE = 67
|
||||
|
||||
@classmethod
|
||||
def FG(cls, col: int) -> str:
|
||||
return f"{cls.CTRL}{30 + col}m"
|
||||
|
||||
@classmethod
|
||||
def BG(cls, col: int) -> str:
|
||||
return f"{cls.CTRL}{40 + col}m"
|
||||
|
||||
@classmethod
|
||||
def FG_RGB(cls, r: int, g: int, b: int) -> str:
|
||||
return f"{cls.CTRL}38;2;{r};{g};{b}m"
|
||||
|
||||
@classmethod
|
||||
def BG_RGB(cls, r: int, g: int, b: int) -> str:
|
||||
return f"{cls.CTRL}48;2;{r};{g};{b}m"
|
||||
+56
-2
@@ -8,10 +8,12 @@ import click
|
||||
|
||||
import midas.ast.midas as m
|
||||
import midas.ast.python as p
|
||||
from midas.ast.location import Location
|
||||
from midas.ast.printer import MidasAstPrinter, MidasPrinter, PythonAstPrinter
|
||||
from midas.checker.checker import Checker
|
||||
from midas.checker.diagnostic import Diagnostic
|
||||
from midas.checker.diagnostic import Diagnostic, DiagnosticType
|
||||
from midas.checker.types import Type
|
||||
from midas.cli.ansi import Ansi
|
||||
from midas.cli.highlighter import (
|
||||
DiagnosticsHighlighter,
|
||||
Highlighter,
|
||||
@@ -32,6 +34,57 @@ def midas():
|
||||
pass
|
||||
|
||||
|
||||
def print_diagnostic(lines: list[str], diagnostic: Diagnostic, indent: int = 4):
|
||||
"""Pretty-print a diagnostic, showing some context if possible
|
||||
|
||||
If the diagnostic concerns a specific part of one line, the line is shown
|
||||
with the affected part highlighted. The message is clearly printed under the
|
||||
line with an underline further indicating the target expression.
|
||||
|
||||
If multiple lines are concerned, no context is shown, only the
|
||||
diagnostic type, location and message
|
||||
|
||||
Args:
|
||||
lines (list[str]): source code lines
|
||||
diagnostic (Diagnostic): the diagnostic to print
|
||||
indent (int, optional): the number of spaces added before the target line to indent if from the location header. Defaults to 4.
|
||||
"""
|
||||
|
||||
loc: Location = diagnostic.location
|
||||
if loc.lineno != loc.end_lineno:
|
||||
print(diagnostic)
|
||||
return
|
||||
|
||||
start_offset: int = loc.col_offset
|
||||
end_offset: int = loc.end_col_offset or (start_offset + 1)
|
||||
|
||||
line: str = lines[loc.lineno - 1]
|
||||
before: str = line[:start_offset]
|
||||
after: str = line[end_offset:]
|
||||
|
||||
color: int = {
|
||||
DiagnosticType.ERROR: Ansi.RED,
|
||||
DiagnosticType.WARNING: Ansi.YELLOW,
|
||||
DiagnosticType.INFO: Ansi.CYAN,
|
||||
}.get(diagnostic.type, Ansi.WHITE)
|
||||
|
||||
subject: str = Ansi.FG(color) + line[start_offset:end_offset] + Ansi.RESET
|
||||
cursor: str = (
|
||||
" " * start_offset
|
||||
+ Ansi.FG(color)
|
||||
+ "~" * (end_offset - start_offset)
|
||||
+ "> "
|
||||
+ diagnostic.message
|
||||
+ Ansi.RESET
|
||||
)
|
||||
|
||||
indent_str: str = " " * indent
|
||||
print(diagnostic.location_str + ":")
|
||||
print(indent_str + before + subject + after)
|
||||
print(indent_str + cursor)
|
||||
print()
|
||||
|
||||
|
||||
@midas.command()
|
||||
@click.option("-l", "--highlight", type=click.File("w"))
|
||||
@click.option("-t", "--types", type=click.File("r"), multiple=True)
|
||||
@@ -57,8 +110,9 @@ def compile(
|
||||
types_paths=types_paths,
|
||||
)
|
||||
diagnostics: list[Diagnostic] = checker.check(stmts)
|
||||
lines: list[str] = source.split("\n")
|
||||
for diagnostic in diagnostics:
|
||||
print(diagnostic)
|
||||
print_diagnostic(lines, diagnostic)
|
||||
|
||||
if verbose:
|
||||
print(
|
||||
|
||||
+27
-6
@@ -3,6 +3,7 @@ from typing import Optional
|
||||
import midas.ast.midas as m
|
||||
from midas.checker.types import (
|
||||
AliasType,
|
||||
Operation,
|
||||
Type,
|
||||
UnknownType,
|
||||
)
|
||||
@@ -14,7 +15,7 @@ class MidasResolver(m.Stmt.Visitor[None], m.Expr.Visitor[None], m.Type.Visitor[T
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._types: dict[str, Type] = {}
|
||||
self._operations: dict[tuple[Type, str, Type], Type] = {}
|
||||
self._operations: dict[Operation.CallSignature, Type] = {}
|
||||
|
||||
define_builtins(self)
|
||||
|
||||
@@ -48,10 +49,26 @@ class MidasResolver(m.Stmt.Visitor[None], m.Expr.Visitor[None], m.Type.Visitor[T
|
||||
Returns:
|
||||
Optional[Type]: the result type, or None if no matching operation was found
|
||||
"""
|
||||
operation: tuple[Type, str, Type] = (left, operator, right)
|
||||
result: Optional[Type] = self._operations.get(operation)
|
||||
signature: Operation.CallSignature = Operation.CallSignature(
|
||||
left=left,
|
||||
method=operator,
|
||||
right=right,
|
||||
)
|
||||
result: Optional[Type] = self._operations.get(signature)
|
||||
return result
|
||||
|
||||
def get_operations_by_name(self, name: str) -> list[Operation]:
|
||||
operations: list[Operation] = []
|
||||
for signature, result in self._operations.items():
|
||||
if signature.method == name:
|
||||
operations.append(
|
||||
Operation(
|
||||
signature=signature,
|
||||
result=result,
|
||||
)
|
||||
)
|
||||
return operations
|
||||
|
||||
def define_type(self, name: str, type: Type) -> Type:
|
||||
"""Define a type in the registry
|
||||
|
||||
@@ -82,12 +99,16 @@ class MidasResolver(m.Stmt.Visitor[None], m.Expr.Visitor[None], m.Type.Visitor[T
|
||||
Raises:
|
||||
ValueError: if an operation is already defined with these operands and name
|
||||
"""
|
||||
operation: tuple[Type, str, Type] = (left, operator, right)
|
||||
if operation in self._operations:
|
||||
signature: Operation.CallSignature = Operation.CallSignature(
|
||||
left=left,
|
||||
method=operator,
|
||||
right=right,
|
||||
)
|
||||
if signature in self._operations:
|
||||
raise ValueError(
|
||||
f"Operation {operator} already defined between {left} and {right}"
|
||||
)
|
||||
self._operations[operation] = result
|
||||
self._operations[signature] = result
|
||||
|
||||
def resolve(self, stmts: list[m.Stmt]):
|
||||
"""Process a sequence of statements
|
||||
|
||||
@@ -13,20 +13,6 @@
|
||||
]
|
||||
},
|
||||
"message": "Cannot assign BaseType(name='str') to c of type BaseType(name='int')"
|
||||
},
|
||||
{
|
||||
"type": "Error",
|
||||
"location": {
|
||||
"start": [
|
||||
9,
|
||||
4
|
||||
],
|
||||
"end": [
|
||||
9,
|
||||
9
|
||||
]
|
||||
},
|
||||
"message": "Undefined operation __add__ between BaseType(name='bool') and BaseType(name='bool')"
|
||||
}
|
||||
],
|
||||
"judgments": [
|
||||
@@ -172,7 +158,9 @@
|
||||
"name": "d"
|
||||
}
|
||||
},
|
||||
"type": {}
|
||||
"type": {
|
||||
"name": "int"
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": {
|
||||
|
||||
@@ -1,34 +1,5 @@
|
||||
{
|
||||
"diagnostics": [
|
||||
{
|
||||
"type": "Error",
|
||||
"location": {
|
||||
"start": [
|
||||
12,
|
||||
5
|
||||
],
|
||||
"end": [
|
||||
12,
|
||||
12
|
||||
]
|
||||
},
|
||||
"message": "Undefined operation __add__ between BaseType(name='int') and BaseType(name='float')"
|
||||
},
|
||||
{
|
||||
"type": "Error",
|
||||
"location": {
|
||||
"start": [
|
||||
12,
|
||||
0
|
||||
],
|
||||
"end": [
|
||||
12,
|
||||
12
|
||||
]
|
||||
},
|
||||
"message": "Cannot assign UnknownType() to v3 of type BaseType(name='float')"
|
||||
}
|
||||
],
|
||||
"diagnostics": [],
|
||||
"judgments": [
|
||||
{
|
||||
"location": {
|
||||
@@ -214,7 +185,9 @@
|
||||
"name": "v2"
|
||||
}
|
||||
},
|
||||
"type": {}
|
||||
"type": {
|
||||
"name": "float"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user