Compare commits

...

10 Commits

14 changed files with 693 additions and 175 deletions

View File

@@ -26,6 +26,14 @@ class MemberKind(Enum):
METHOD = auto() METHOD = auto()
@dataclass(frozen=True, kw_only=True)
class ParamSpec:
l_paren: Token
pos: list[FunctionType.Argument]
mixed: list[FunctionType.Argument]
kw: list[FunctionType.Argument]
###< ###<
@@ -50,9 +58,8 @@ class ExtendStmt:
class PredicateStmt: class PredicateStmt:
name: Token name: Token
subject: Token params: list[ParamSpec]
type: Type body: Expr
condition: Expr
###< ###<
@@ -78,6 +85,12 @@ class UnaryExpr:
right: Expr right: Expr
class CallExpr:
callee: Expr
arguments: list[Expr]
keywords: dict[str, Expr]
class GetExpr: class GetExpr:
expr: Expr expr: Expr
name: Token name: Token
@@ -128,9 +141,7 @@ class ExtensionType:
class FunctionType: class FunctionType:
pos_args: list[Argument] params: ParamSpec
args: list[Argument]
kw_args: list[Argument]
returns: Type returns: Type
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)

View File

@@ -27,6 +27,14 @@ class MemberKind(Enum):
METHOD = auto() METHOD = auto()
@dataclass(frozen=True, kw_only=True)
class ParamSpec:
l_paren: Token
pos: list[FunctionType.Argument]
mixed: list[FunctionType.Argument]
kw: list[FunctionType.Argument]
############## ##############
# Statements # # Statements #
############## ##############
@@ -86,9 +94,8 @@ class ExtendStmt(Stmt):
@dataclass(frozen=True) @dataclass(frozen=True)
class PredicateStmt(Stmt): class PredicateStmt(Stmt):
name: Token name: Token
subject: Token params: list[ParamSpec]
type: Type body: Expr
condition: Expr
def accept(self, visitor: Stmt.Visitor[T]) -> T: def accept(self, visitor: Stmt.Visitor[T]) -> T:
return visitor.visit_predicate_stmt(self) return visitor.visit_predicate_stmt(self)
@@ -116,6 +123,9 @@ class Expr(ABC):
@abstractmethod @abstractmethod
def visit_unary_expr(self, expr: UnaryExpr) -> T: ... def visit_unary_expr(self, expr: UnaryExpr) -> T: ...
@abstractmethod
def visit_call_expr(self, expr: CallExpr) -> T: ...
@abstractmethod @abstractmethod
def visit_get_expr(self, expr: GetExpr) -> T: ... def visit_get_expr(self, expr: GetExpr) -> T: ...
@@ -161,6 +171,16 @@ class UnaryExpr(Expr):
return visitor.visit_unary_expr(self) return visitor.visit_unary_expr(self)
@dataclass(frozen=True)
class CallExpr(Expr):
callee: Expr
arguments: list[Expr]
keywords: dict[str, Expr]
def accept(self, visitor: Expr.Visitor[T]) -> T:
return visitor.visit_call_expr(self)
@dataclass(frozen=True) @dataclass(frozen=True)
class GetExpr(Expr): class GetExpr(Expr):
expr: Expr expr: Expr
@@ -279,9 +299,7 @@ class ExtensionType(Type):
@dataclass(frozen=True) @dataclass(frozen=True)
class FunctionType(Type): class FunctionType(Type):
pos_args: list[Argument] params: ParamSpec
args: list[Argument]
kw_args: list[Argument]
returns: Type returns: Type
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)

View File

@@ -150,13 +150,17 @@ class MidasAstPrinter(
self._write_line("PredicateStmt") self._write_line("PredicateStmt")
with self._child_level(): with self._child_level():
self._write_line(f'name: "{stmt.name.lexeme}"') self._write_line(f'name: "{stmt.name.lexeme}"')
self._write_line(f'subject: "{stmt.subject.lexeme}"') self._write_line("params")
self._write_line("type") with self._child_level():
for i, spec in enumerate(stmt.params):
self._idx = i
if i == len(stmt.params) - 1:
self._mark_last()
self._visit_param_spec(spec)
self._write_line("body", last=True)
with self._child_level(single=True): with self._child_level(single=True):
stmt.type.accept(self) stmt.body.accept(self)
self._write_line("condition", last=True)
with self._child_level(single=True):
stmt.condition.accept(self)
# Expressions # Expressions
@@ -195,6 +199,29 @@ class MidasAstPrinter(
with self._child_level(single=True): with self._child_level(single=True):
expr.right.accept(self) expr.right.accept(self)
def visit_call_expr(self, expr: m.CallExpr) -> None:
self._write_line("CallExpr")
with self._child_level():
self._write_line("callee")
with self._child_level(single=True):
expr.callee.accept(self)
self._write_line("arguments")
with self._child_level():
for i, arg in enumerate(expr.arguments):
self._idx = i
if i == len(expr.arguments) - 1:
self._mark_last()
arg.accept(self)
self._write_line("keywords", last=True)
with self._child_level():
for i, (name, arg) in enumerate(expr.keywords.items()):
self._idx = i
if i == len(expr.keywords) - 1:
self._mark_last()
self._write_line(name)
with self._child_level(single=True):
arg.accept(self)
def visit_get_expr(self, expr: m.GetExpr): def visit_get_expr(self, expr: m.GetExpr):
self._write_line("GetExpr") self._write_line("GetExpr")
with self._child_level(): with self._child_level():
@@ -276,34 +303,41 @@ class MidasAstPrinter(
def visit_function_type(self, type: m.FunctionType) -> None: def visit_function_type(self, type: m.FunctionType) -> None:
self._write_line("FunctionType") self._write_line("FunctionType")
with self._child_level(): with self._child_level():
self._write_line("pos_args") self._write_line("params")
with self._child_level(): with self._child_level(single=True):
for i, arg in enumerate(type.pos_args): self._visit_param_spec(type.params)
self._idx = i
if i == len(type.pos_args) - 1:
self._mark_last()
self._print_function_arg(arg)
self._write_line("args")
with self._child_level():
for i, arg in enumerate(type.args):
self._idx = i
if i == len(type.args) - 1:
self._mark_last()
self._print_function_arg(arg)
self._write_line("kw_args")
with self._child_level():
for i, arg in enumerate(type.kw_args):
self._idx = i
if i == len(type.kw_args) - 1:
self._mark_last()
self._print_function_arg(arg)
self._write_line("returns", last=True) self._write_line("returns", last=True)
with self._child_level(single=True): with self._child_level(single=True):
type.returns.accept(self) type.returns.accept(self)
def _visit_param_spec(self, spec: m.ParamSpec) -> None:
self._write_line("ParamSpec")
with self._child_level():
self._write_line("pos")
with self._child_level():
for i, arg in enumerate(spec.pos):
self._idx = i
if i == len(spec.pos) - 1:
self._mark_last()
self._print_function_arg(arg)
self._write_line("mixed")
with self._child_level():
for i, arg in enumerate(spec.mixed):
self._idx = i
if i == len(spec.mixed) - 1:
self._mark_last()
self._print_function_arg(arg)
self._write_line("kw", last=True)
with self._child_level():
for i, arg in enumerate(spec.kw):
self._idx = i
if i == len(spec.kw) - 1:
self._mark_last()
self._print_function_arg(arg)
def _print_function_arg(self, arg: m.FunctionType.Argument) -> None: def _print_function_arg(self, arg: m.FunctionType.Argument) -> None:
self._write_line("Argument") self._write_line("Argument")
with self._child_level(): with self._child_level():
@@ -367,10 +401,9 @@ class MidasPrinter(m.Expr.Visitor[str], m.Stmt.Visitor[str], m.Type.Visitor[str]
def visit_predicate_stmt(self, stmt: m.PredicateStmt): def visit_predicate_stmt(self, stmt: m.PredicateStmt):
name: str = stmt.name.lexeme name: str = stmt.name.lexeme
subject: str = stmt.subject.lexeme sig: str = "".join(self._visit_param_spec(spec) for spec in stmt.params)
type: str = stmt.type.accept(self) body: str = stmt.body.accept(self)
condition: str = stmt.condition.accept(self) return self.indented(f"predicate {name}{sig} = {body}")
return self.indented(f"predicate {name}({subject}: {type}) = {condition}")
def visit_logical_expr(self, expr: m.LogicalExpr): def visit_logical_expr(self, expr: m.LogicalExpr):
left: str = expr.left.accept(self) left: str = expr.left.accept(self)
@@ -389,6 +422,12 @@ class MidasPrinter(m.Expr.Visitor[str], m.Stmt.Visitor[str], m.Type.Visitor[str]
right: str = expr.right.accept(self) right: str = expr.right.accept(self)
return f"{operator}{right}" return f"{operator}{right}"
def visit_call_expr(self, expr: m.CallExpr) -> str:
args: list[str] = [arg.accept(self) for arg in expr.arguments] + [
f"{name}={arg.accept(self)}" for name, arg in expr.keywords.items()
]
return f"{expr.callee.accept(self)}({', '.join(args)})"
def visit_get_expr(self, expr: m.GetExpr): def visit_get_expr(self, expr: m.GetExpr):
expr_: str = expr.expr.accept(self) expr_: str = expr.expr.accept(self)
name: str = expr.name.lexeme name: str = expr.name.lexeme
@@ -436,9 +475,13 @@ class MidasPrinter(m.Expr.Visitor[str], m.Stmt.Visitor[str], m.Type.Visitor[str]
return f"{type.base.accept(self)} & {type.extension.accept(self)}" return f"{type.base.accept(self)} & {type.extension.accept(self)}"
def visit_function_type(self, type: m.FunctionType) -> str: def visit_function_type(self, type: m.FunctionType) -> str:
pos_args: list[str] = [self._print_arg(arg) for arg in type.pos_args] spec: str = self._visit_param_spec(type.params)
mixed_args: list[str] = [self._print_arg(arg) for arg in type.args] return f"fn {spec} -> {type.returns.accept(self)}"
kw_args: list[str] = [self._print_arg(arg) for arg in type.kw_args]
def _visit_param_spec(self, spec: m.ParamSpec) -> str:
pos_args: list[str] = [self._print_arg(arg) for arg in spec.pos]
mixed_args: list[str] = [self._print_arg(arg) for arg in spec.mixed]
kw_args: list[str] = [self._print_arg(arg) for arg in spec.kw]
args: list[str] = pos_args args: list[str] = pos_args
if len(pos_args) != 0: if len(pos_args) != 0:
@@ -447,8 +490,7 @@ class MidasPrinter(m.Expr.Visitor[str], m.Stmt.Visitor[str], m.Type.Visitor[str]
if len(kw_args) != 0: if len(kw_args) != 0:
args.append("*") args.append("*")
args += kw_args args += kw_args
return f"({', '.join(args)})"
return f"fn ({', '.join(args)}) -> {type.returns.accept(self)}"
def _print_arg(self, arg: m.FunctionType.Argument) -> str: def _print_arg(self, arg: m.FunctionType.Argument) -> str:
res: str = "" res: str = ""

View File

@@ -1,4 +1,5 @@
import logging import logging
from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
@@ -9,9 +10,11 @@ from midas.checker.reporter import FileReporter, Reporter
from midas.checker.types import ( from midas.checker.types import (
AliasType, AliasType,
ComplexType, ComplexType,
ConstraintType,
ExtensionType, ExtensionType,
Function, Function,
GenericType, GenericType,
Predicate,
Type, Type,
TypeVar, TypeVar,
UnknownType, UnknownType,
@@ -21,6 +24,13 @@ from midas.lexer.token import Token
from midas.parser.midas import MidasParser from midas.parser.midas import MidasParser
@dataclass(frozen=True, kw_only=True)
class TypedParamSpec:
pos: list[Function.Argument]
mixed: list[Function.Argument]
kw: list[Function.Argument]
class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[None], m.Type.Visitor[Type]): class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[None], m.Type.Visitor[Type]):
"""A resolver which evaluates Midas type definitions and build a registry""" """A resolver which evaluates Midas type definitions and build a registry"""
@@ -37,6 +47,8 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[None], m.Type.Visitor[Type
builtins_path: Path = (Path(__file__).parent / "builtins.midas").resolve() builtins_path: Path = (Path(__file__).parent / "builtins.midas").resolve()
self.process(builtins_path.read_text(), str(builtins_path)) self.process(builtins_path.read_text(), str(builtins_path))
self._bool: Type = self.get_type("bool")
def process(self, source: str, path: Optional[str]): def process(self, source: str, path: Optional[str]):
self.reporter = self.reporter.for_file(path) self.reporter = self.reporter.for_file(path)
lexer: MidasLexer = MidasLexer(source) lexer: MidasLexer = MidasLexer(source)
@@ -106,7 +118,24 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[None], m.Type.Visitor[Type
) )
def visit_predicate_stmt(self, stmt: m.PredicateStmt) -> None: def visit_predicate_stmt(self, stmt: m.PredicateStmt) -> None:
self.reporter.warning(stmt.location, "PredicateStmt not yet supported") params: list[TypedParamSpec] = [
self._visit_param_spec(spec) for spec in stmt.params
]
type: Type = self._bool
for spec in reversed(params):
type = Function(
pos_args=spec.pos,
args=spec.mixed,
kw_args=spec.kw,
returns=type,
)
self.types.define_predicate(
stmt.name.lexeme,
Predicate(
type=type,
body=stmt.body,
),
)
def visit_logical_expr(self, expr: m.LogicalExpr) -> None: def visit_logical_expr(self, expr: m.LogicalExpr) -> None:
self.reporter.warning(expr.location, "LogicalExpr not yet supported") self.reporter.warning(expr.location, "LogicalExpr not yet supported")
@@ -117,6 +146,9 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[None], m.Type.Visitor[Type
def visit_unary_expr(self, expr: m.UnaryExpr) -> None: def visit_unary_expr(self, expr: m.UnaryExpr) -> None:
self.reporter.warning(expr.location, "UnaryExpr not yet supported") self.reporter.warning(expr.location, "UnaryExpr not yet supported")
def visit_call_expr(self, expr: m.CallExpr) -> None:
self.reporter.warning(expr.location, "CallExpr not yet supported")
def visit_get_expr(self, expr: m.GetExpr) -> None: def visit_get_expr(self, expr: m.GetExpr) -> None:
self.reporter.warning(expr.location, "GetExpr not yet supported") self.reporter.warning(expr.location, "GetExpr not yet supported")
@@ -153,10 +185,10 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[None], m.Type.Visitor[Type
return UnknownType() return UnknownType()
def visit_constraint_type(self, type: m.ConstraintType) -> Type: def visit_constraint_type(self, type: m.ConstraintType) -> Type:
type_: Type = type.type.accept(self) return ConstraintType(
type.constraint.accept(self) type=type.type.accept(self),
# TODO constraint=type.constraint,
return UnknownType() )
def visit_complex_type(self, type: m.ComplexType) -> ComplexType: def visit_complex_type(self, type: m.ComplexType) -> ComplexType:
return ComplexType( return ComplexType(
@@ -172,8 +204,17 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[None], m.Type.Visitor[Type
) )
def visit_function_type(self, type: m.FunctionType) -> Type: def visit_function_type(self, type: m.FunctionType) -> Type:
n_pos_args: int = len(type.pos_args) params: TypedParamSpec = self._visit_param_spec(type.params)
n_args: int = len(type.args) return Function(
pos_args=params.pos,
args=params.mixed,
kw_args=params.kw,
returns=type.returns.accept(self),
)
def _visit_param_spec(self, spec: m.ParamSpec) -> TypedParamSpec:
n_pos: int = len(spec.pos)
n_mixed: int = len(spec.mixed)
def process_arg(arg: m.FunctionType.Argument, i: int) -> Function.Argument: def process_arg(arg: m.FunctionType.Argument, i: int) -> Function.Argument:
return Function.Argument( return Function.Argument(
@@ -183,14 +224,10 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[None], m.Type.Visitor[Type
required=arg.required, required=arg.required,
) )
return Function( return TypedParamSpec(
pos_args=[process_arg(arg, i) for i, arg in enumerate(type.pos_args)], pos=[process_arg(arg, i) for i, arg in enumerate(spec.pos)],
args=[process_arg(arg, i + n_pos_args) for i, arg in enumerate(type.args)], mixed=[process_arg(arg, i + n_pos) for i, arg in enumerate(spec.mixed)],
kw_args=[ kw=[process_arg(arg, i + n_pos + n_mixed) for i, arg in enumerate(spec.kw)],
process_arg(arg, i + n_pos_args + n_args)
for i, arg in enumerate(type.kw_args)
],
returns=type.returns.accept(self),
) )
def _resolve_type_params(self, params: list[m.TypeParam]): def _resolve_type_params(self, params: list[m.TypeParam]):

View File

@@ -11,6 +11,7 @@ from midas.checker.types import (
Function, Function,
GenericType, GenericType,
OverloadedFunction, OverloadedFunction,
Predicate,
TopType, TopType,
Type, Type,
TypeVar, TypeVar,
@@ -24,6 +25,7 @@ class TypesRegistry:
self.logger: logging.Logger = logging.getLogger("TypesRegistry") self.logger: logging.Logger = logging.getLogger("TypesRegistry")
self._types: dict[str, Type] = {} self._types: dict[str, Type] = {}
self._members: dict[str, dict[str, Type]] = {} self._members: dict[str, dict[str, Type]] = {}
self._predicates: dict[str, Predicate] = {}
def get_type(self, name: str) -> Type: def get_type(self, name: str) -> Type:
"""Get a type from its name """Get a type from its name
@@ -81,6 +83,11 @@ class TypesRegistry:
else: else:
members[member_name] = member_type members[member_name] = member_type
def define_predicate(self, name: str, predicate: Predicate):
if name in self._predicates:
raise ValueError(f"Predicate {name} already defined")
self._predicates[name] = predicate
def is_subtype(self, type1: Type, type2: Type) -> bool: def is_subtype(self, type1: Type, type2: Type) -> bool:
"""Check whether `type1` is a subtype of `type2` """Check whether `type1` is a subtype of `type2`
@@ -345,3 +352,6 @@ class TypesRegistry:
case _: case _:
self.logger.debug(f"Can't get member on {type}") self.logger.debug(f"Can't get member on {type}")
return None return None
def lookup_predicate(self, name: str) -> Optional[Predicate]:
return self._predicates.get(name)

View File

@@ -1,7 +1,10 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Optional from typing import Optional, assert_never
import midas.ast.midas as m
from midas.ast.printer import MidasPrinter
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
@@ -130,6 +133,16 @@ class AppliedType:
return f"{self.name}[{', '.join(map(str, self.args))}]" return f"{self.name}[{', '.join(map(str, self.args))}]"
@dataclass(frozen=True, kw_only=True)
class ConstraintType:
type: Type
constraint: m.Expr
def __str__(self) -> str:
printer = MidasPrinter()
return f"{self.type} where {printer.print(self.constraint)}"
def substitute_typevars(type: Type, substitutions: dict[str, Type]) -> Type: def substitute_typevars(type: Type, substitutions: dict[str, Type]) -> Type:
def sub_argument(arg: Function.Argument): def sub_argument(arg: Function.Argument):
return Function.Argument( return Function.Argument(
@@ -195,6 +208,12 @@ def substitute_typevars(type: Type, substitutions: dict[str, Type]) -> Type:
body=substitute_typevars(body, substitutions), body=substitute_typevars(body, substitutions),
) )
case ConstraintType():
return ConstraintType(
type=substitute_typevars(type.type, substitutions),
constraint=type.constraint,
)
case TypeVar(name=name): case TypeVar(name=name):
if name in substitutions: if name in substitutions:
return substitutions[name] return substitutions[name]
@@ -203,9 +222,13 @@ def substitute_typevars(type: Type, substitutions: dict[str, Type]) -> Type:
case UnknownType() | UnitType(): case UnknownType() | UnitType():
return type return type
case _: case TopType() | GenericType():
raise NotImplementedError(f"Unsupported type {type}") raise NotImplementedError(f"Unsupported type {type}")
# Ensure exhaustiveness
case _:
assert_never(type)
def unfold_type(type: Type) -> Type: def unfold_type(type: Type) -> Type:
match type: match type:
@@ -215,6 +238,12 @@ def unfold_type(type: Type) -> Type:
return type return type
@dataclass(frozen=True, kw_only=True)
class Predicate:
type: Type
body: m.Expr
Type = ( Type = (
TopType TopType
| BaseType | BaseType
@@ -228,4 +257,5 @@ Type = (
| TypeVar | TypeVar
| GenericType | GenericType
| AppliedType | AppliedType
| ConstraintType
) )

View File

@@ -38,5 +38,5 @@ def compile(
if any(map(lambda d: d.type == DiagnosticType.ERROR, diagnostics)): if any(map(lambda d: d.type == DiagnosticType.ERROR, diagnostics)):
sys.exit(1) sys.exit(1)
generator = Generator(workdir=source_path.parent) generator = Generator(workdir=source_path.parent, types=checker.types)
generator.generate(typed_ast, source_path) generator.generate(typed_ast, source_path)

View File

@@ -0,0 +1,188 @@
import ast
from typing import Optional
import midas.ast.midas as m
from midas.checker.registry import TypesRegistry
from midas.checker.types import Function, Predicate, Type
from midas.lexer.token import TokenType
LOGICAL_OPERATORS: dict[TokenType, type[ast.boolop]] = {
TokenType.AND: ast.And,
# TokenType.OR: ast.Or,
}
BINARY_OPERATORS: dict[TokenType, type[ast.operator]] = {
# TokenType.PLUS: ast.Add,
TokenType.MINUS: ast.Sub,
TokenType.STAR: ast.Mult,
TokenType.SLASH: ast.Div,
}
UNARY_OPERATORS: dict[TokenType, type[ast.unaryop]] = {
# TokenType.PLUS: ast.UAdd,
TokenType.MINUS: ast.USub,
}
COMPARISON_OPERATORS: dict[TokenType, type[ast.cmpop]] = {
TokenType.GREATER: ast.Gt,
TokenType.GREATER_EQUAL: ast.GtE,
TokenType.LESS: ast.Lt,
TokenType.LESS_EQUAL: ast.LtE,
TokenType.EQUAL_EQUAL: ast.Eq,
TokenType.BANG_EQUAL: ast.NotEq,
}
class ConstraintGenerator(m.Expr.Visitor[ast.expr]):
def __init__(self, types: TypesRegistry):
self.types: TypesRegistry = types
self._id: int = 0
self._definitions: list[ast.stmt] = []
self._aliases: dict[str, str] = {}
def get_definitions(self) -> list[ast.stmt]:
return self._definitions
def generate(self, expr: m.Expr) -> ast.expr:
match expr:
case m.VariableExpr():
return expr.accept(self)
case _:
func = Function(
pos_args=[],
args=[
Function.Argument(
pos=0,
name="_",
type=self.types.get_type("Any"),
required=True,
)
],
kw_args=[],
returns=self.types.get_type("bool"),
)
alias: str = self.make_alias(None)
definition: ast.stmt = self.make_definition(
alias, Predicate(type=func, body=expr)
)
self._definitions.append(definition)
return ast.Name(id=alias)
def make_alias(self, name: Optional[str]) -> str:
suffix: str = f"_{name}" if name is not None else ""
alias: str = f"__midas_p{self._id}{suffix}__"
self._id += 1
return alias
def make_definition(self, name: str, predicate: Predicate) -> ast.stmt:
body: list[ast.stmt] = [ast.Return(value=predicate.body.accept(self))]
return self.make_func(name, body, predicate.type)
def make_args(self, func: Function) -> ast.arguments:
return ast.arguments(
posonlyargs=[ast.arg(arg=arg.name) for arg in func.pos_args],
args=[ast.arg(arg=arg.name) for arg in func.args],
kwonlyargs=[ast.arg(arg=arg.name) for arg in func.kw_args],
defaults=[],
kw_defaults=[],
)
def make_func(
self, name: str, inner_body: list[ast.stmt], type: Type, level: int = 0
) -> ast.stmt:
match type:
case Function(returns=Function()):
inner_name: str = f"inner{level}"
return ast.FunctionDef(
name=name,
args=self.make_args(type),
body=[
self.make_func(inner_name, inner_body, type.returns, level + 1),
ast.Return(value=ast.Name(id=inner_name)),
],
decorator_list=[],
)
case Function():
return ast.FunctionDef(
name=name,
args=self.make_args(type),
body=inner_body,
decorator_list=[],
)
case _:
raise ValueError(f"Expected function, got {type}")
def get_predicate(self, name: str) -> Optional[ast.expr]:
if name not in self._aliases:
predicate: Optional[Predicate] = self.types.lookup_predicate(name)
if predicate is None:
return None
alias: str = self.make_alias(name)
self._aliases[name] = alias
self._definitions.append(self.make_definition(alias, predicate))
return ast.Name(id=self._aliases[name])
def visit_logical_expr(self, expr: m.LogicalExpr) -> ast.expr:
return ast.BoolOp(
op=LOGICAL_OPERATORS[expr.operator.type](),
values=[
expr.left.accept(self),
expr.right.accept(self),
],
)
def visit_binary_expr(self, expr: m.BinaryExpr) -> ast.expr:
op: TokenType = expr.operator.type
if op in BINARY_OPERATORS:
return ast.BinOp(
left=expr.left.accept(self),
op=BINARY_OPERATORS[op](),
right=expr.right.accept(self),
)
if op in COMPARISON_OPERATORS:
return ast.Compare(
left=expr.left.accept(self),
ops=[COMPARISON_OPERATORS[op]()],
comparators=[expr.right.accept(self)],
)
raise ValueError(f"Unexpected binary operator {op}")
def visit_unary_expr(self, expr: m.UnaryExpr) -> ast.expr:
return ast.UnaryOp(
op=UNARY_OPERATORS[expr.operator.type](),
operand=expr.right.accept(self),
)
def visit_call_expr(self, expr: m.CallExpr) -> ast.expr:
return ast.Call(
func=expr.callee.accept(self),
args=[arg.accept(self) for arg in expr.arguments],
keywords=[
ast.keyword(arg=name, value=arg.accept(self))
for name, arg in expr.keywords.items()
],
)
def visit_get_expr(self, expr: m.GetExpr) -> ast.expr:
return ast.Attribute(
value=expr.expr.accept(self),
attr=expr.name.lexeme,
)
def visit_variable_expr(self, expr: m.VariableExpr) -> ast.expr:
name: str = expr.name.lexeme
if (p := self.get_predicate(name)) is not None:
return p
return ast.Name(id=name)
def visit_grouping_expr(self, expr: m.GroupingExpr) -> ast.expr:
return expr.accept(self)
def visit_literal_expr(self, expr: m.LiteralExpr) -> ast.expr:
return ast.Constant(value=expr.value)
def visit_wildcard_expr(self, expr: m.WildcardExpr) -> ast.expr:
return ast.Name(id="_")

View File

@@ -2,15 +2,19 @@ import ast
import shutil import shutil
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional, assert_never
import midas.ast.midas as m
import midas.ast.python as p import midas.ast.python as p
from midas.ast.location import Location from midas.ast.location import Location
from midas.ast.printer import MidasPrinter
from midas.checker.registry import TypesRegistry
from midas.checker.types import ( from midas.checker.types import (
AliasType, AliasType,
AppliedType, AppliedType,
BaseType, BaseType,
ComplexType, ComplexType,
ConstraintType,
ExtensionType, ExtensionType,
Function, Function,
GenericType, GenericType,
@@ -19,7 +23,9 @@ from midas.checker.types import (
Type, Type,
TypeVar, TypeVar,
UnitType, UnitType,
UnknownType,
) )
from midas.generator.constraints import ConstraintGenerator
from midas.utils import TypedAST from midas.utils import TypedAST
@@ -30,7 +36,7 @@ class Scope:
class Generator(p.Stmt.Visitor[ast.stmt], p.Expr.Visitor[ast.expr]): class Generator(p.Stmt.Visitor[ast.stmt], p.Expr.Visitor[ast.expr]):
def __init__(self, workdir: Path) -> None: def __init__(self, workdir: Path, types: TypesRegistry) -> None:
self.workdir: Path = workdir.resolve() self.workdir: Path = workdir.resolve()
self.build_dir: Path = self.workdir / "build" / "midas" self.build_dir: Path = self.workdir / "build" / "midas"
if self.build_dir.exists(): if self.build_dir.exists():
@@ -43,13 +49,18 @@ class Generator(p.Stmt.Visitor[ast.stmt], p.Expr.Visitor[ast.expr]):
judgements=[], judgements=[],
) )
self._alias_count: int = 0 self._alias_count: int = 0
self._predicate_count: int = 0
self._scopes: list[Scope] = [] self._scopes: list[Scope] = []
self._constraint_generator: ConstraintGenerator = ConstraintGenerator(types)
self._constraints: list[tuple[m.Expr, ast.expr]] = []
def generate_ast(self, typed_ast: TypedAST, src_path: Path) -> ast.AST: def generate_ast(self, typed_ast: TypedAST, src_path: Path) -> ast.AST:
self.rel_src_path = src_path.relative_to(self.workdir) self.rel_src_path = src_path.relative_to(self.workdir)
self._typed_ast = typed_ast self._typed_ast = typed_ast
body: list[ast.stmt] = self._visit_body(typed_ast.stmts) body: list[ast.stmt] = self._visit_body(typed_ast.stmts)
module = ast.Module(body=body, type_ignores=[]) predicates: list[ast.stmt] = self._constraint_generator.get_definitions()
module = ast.Module(body=predicates + body, type_ignores=[])
module = ast.fix_missing_locations(module) module = ast.fix_missing_locations(module)
return module return module
@@ -246,7 +257,7 @@ class Generator(p.Stmt.Visitor[ast.stmt], p.Expr.Visitor[ast.expr]):
return generated return generated
def _make_alias(self, expr: ast.expr) -> ast.expr: def _make_alias(self, expr: ast.expr) -> ast.expr:
name: str = f"__midas_alias_{self._alias_count}__" name: str = f"__midas_a{self._alias_count}__"
alias = ast.Name(id=name) alias = ast.Name(id=name)
self._alias_count += 1 self._alias_count += 1
self._scopes[-1].aliases.append(name) self._scopes[-1].aliases.append(name)
@@ -276,6 +287,9 @@ class Generator(p.Stmt.Visitor[ast.stmt], p.Expr.Visitor[ast.expr]):
def _make_cast_asserts(self, src_location: Location, expr: ast.expr, type: Type): def _make_cast_asserts(self, src_location: Location, expr: ast.expr, type: Type):
match type: match type:
case UnknownType():
pass
case BaseType(name=name): case BaseType(name=name):
self._add_assert( self._add_assert(
ast.Call( ast.Call(
@@ -301,8 +315,15 @@ class Generator(p.Stmt.Visitor[ast.stmt], p.Expr.Visitor[ast.expr]):
self._make_cast_assert_message(src_location, expr, type), self._make_cast_assert_message(src_location, expr, type),
) )
case AppliedType(): case AppliedType(body=body):
self._make_cast_asserts(src_location, expr, type.body) self._make_cast_asserts(src_location, expr, body)
case ConstraintType(type=base, constraint=constraint):
self._make_cast_asserts(src_location, expr, base)
self._make_constraint_assert(src_location, expr, constraint)
case TypeVar():
raise RuntimeError("Unexpected TypeVar")
case ( case (
TopType() TopType()
@@ -314,8 +335,9 @@ class Generator(p.Stmt.Visitor[ast.stmt], p.Expr.Visitor[ast.expr]):
): ):
raise NotImplementedError(f"Can't make assertion for type {type}") raise NotImplementedError(f"Can't make assertion for type {type}")
case TypeVar(): # Ensure exhaustiveness
raise RuntimeError("Unexpected TypeVar") case _:
assert_never(type)
def _make_cast_assert_message( def _make_cast_assert_message(
self, location: Location, expr: ast.expr, type: Type self, location: Location, expr: ast.expr, type: Type
@@ -339,3 +361,36 @@ class Generator(p.Stmt.Visitor[ast.stmt], p.Expr.Visitor[ast.expr]):
ast.Constant(f" to {type}"), ast.Constant(f" to {type}"),
] ]
) )
def _make_constraint_assert(
self, src_location: Location, expr: ast.expr, constraint: m.Expr
):
test_func: ast.expr = self._get_constraint(constraint)
self._add_assert(
ast.Call(
func=test_func,
args=[expr],
keywords=[],
),
self._make_constraint_assert_message(src_location, expr, constraint),
)
def _make_constraint_assert_message(
self, location: Location, expr: ast.expr, constraint: m.Expr
) -> ast.expr:
printer = MidasPrinter()
constraint_str: str = printer.print(constraint)
loc_str: str = f"{self.rel_src_path}:L{location.lineno}:{location.col_offset+1}"
# f"file.py:L1:1: ConstraintError: Value does not fit constraint 'v > 0'"
return ast.Constant(
f"{loc_str}: ConstraintError: Value does not fit constraint '{constraint_str}'"
)
def _get_constraint(self, expr: m.Expr) -> ast.expr:
for expr2, constraint in self._constraints:
if expr2 == expr:
return constraint
constraint: ast.expr = self._constraint_generator.generate(expr)
self._constraints.append((expr, constraint))
return constraint

View File

@@ -3,6 +3,7 @@ from typing import Optional
from midas.ast.location import Location from midas.ast.location import Location
from midas.ast.midas import ( from midas.ast.midas import (
BinaryExpr, BinaryExpr,
CallExpr,
ComplexType, ComplexType,
ConstraintType, ConstraintType,
Expr, Expr,
@@ -17,6 +18,7 @@ from midas.ast.midas import (
MemberKind, MemberKind,
MemberStmt, MemberStmt,
NamedType, NamedType,
ParamSpec,
PredicateStmt, PredicateStmt,
Stmt, Stmt,
Type, Type,
@@ -265,6 +267,9 @@ class MidasParser(Parser):
Returns: Returns:
Expr: the parsed constraint expression Expr: the parsed constraint expression
""" """
return self.expression()
def expression(self) -> Expr:
return self.and_() return self.and_()
def and_(self) -> Expr: def and_(self) -> Expr:
@@ -331,7 +336,55 @@ class MidasParser(Parser):
right: Expr = self.unary() right: Expr = self.unary()
location: Location = Location.span(operator.get_location(), right.location) location: Location = Location.span(operator.get_location(), right.location)
return UnaryExpr(location=location, operator=operator, right=right) return UnaryExpr(location=location, operator=operator, right=right)
return self.reference() return self.call()
def call(self) -> Expr:
expr: Expr = self.reference()
while self.match(TokenType.LEFT_PAREN):
expr = self.finish_call(expr)
return expr
def finish_call(self, callee: Expr) -> Expr:
l_paren: Token = self.previous()
pos_args: list[Expr] = []
kw_args: dict[str, Expr] = {}
keywords: bool = False
while not self.match(TokenType.RIGHT_PAREN):
if self.check_identifier() and self.check_next(TokenType.EQUAL):
keywords = True
keyword: Token = self.advance()
value: Expr = self.expression()
name: str = keyword.lexeme
if name in kw_args:
self.error(
self.peek(),
f"Multiple values passed for '{name}', only the last occurrence will be used",
)
kw_args[name] = value
else:
value = self.expression()
if self.check(TokenType.EQUAL):
if keywords:
raise self.error(self.peek(), "Invalid keyword argument name")
else:
raise self.error(
self.peek(),
"Cannot pass positional arguments after a keyword argument",
)
pos_args.append(value)
if not self.match(TokenType.COMMA):
break
r_paren: Token = self.consume(
TokenType.RIGHT_PAREN, "Expected ')' after arguments."
)
return CallExpr(
location=l_paren.location_to(r_paren),
callee=callee,
arguments=pos_args,
keywords=kw_args,
)
def reference(self) -> Expr: def reference(self) -> Expr:
"""Parse an attribute access expression or a simpler expression """Parse an attribute access expression or a simpler expression
@@ -453,23 +506,35 @@ class MidasParser(Parser):
PredicateStmt: the parsed predicate declaration statement PredicateStmt: the parsed predicate declaration statement
""" """
keyword: Token = self.previous() keyword: Token = self.previous()
name: Token = self.consume_identifier("Expected predicate name") name: Token = self.consume_identifier("Expected predicate name")
self.consume(TokenType.LEFT_PAREN, "Expected '(' before predicate subject")
subject: Token = self.consume_identifier("Expected subject name") params: list[ParamSpec] = []
self.consume(TokenType.COLON, "Expected ':' after subject name") while self.check(TokenType.LEFT_PAREN):
type: Type = self.type_expr() params.append(self.function_args())
self.consume(TokenType.RIGHT_PAREN, "Expected ')' after predicate subject")
self.consume(TokenType.EQUAL, "Expected '=' after predicate subject") self.consume(TokenType.EQUAL, "Expected '=' after predicate subject")
condition: Expr = self.constraint() body: Expr = self.constraint()
return PredicateStmt( return PredicateStmt(
location=keyword.location_to(self.previous()), location=keyword.location_to(self.previous()),
name=name, name=name,
subject=subject, params=params,
type=type, body=body,
condition=condition,
) )
def function(self) -> FunctionType: def function(self) -> FunctionType:
params: ParamSpec = self.function_args()
self.consume(TokenType.ARROW, "Expected '->' before result type")
result: Type = self.type_expr()
return FunctionType(
location=params.l_paren.location_to(self.previous()),
params=params,
returns=result,
)
def function_args(self) -> ParamSpec:
l_paren: Token = self.consume( l_paren: Token = self.consume(
TokenType.LEFT_PAREN, "Expected '(' before function parameters" TokenType.LEFT_PAREN, "Expected '(' before function parameters"
) )
@@ -526,14 +591,4 @@ class MidasParser(Parser):
self.error(token, "Unnamed mixed argument") self.error(token, "Unnamed mixed argument")
self.consume(TokenType.RIGHT_PAREN, "Expected ')' after function parameters") self.consume(TokenType.RIGHT_PAREN, "Expected ')' after function parameters")
return ParamSpec(l_paren=l_paren, pos=pos_args, mixed=args, kw=kw_args)
self.consume(TokenType.ARROW, "Expected '->' before result type")
result: Type = self.type_expr()
return FunctionType(
location=l_paren.location_to(self.previous()),
pos_args=pos_args,
args=args,
kw_args=kw_args,
returns=result,
)

View File

@@ -9,13 +9,13 @@ Module(
level=0), level=0),
Assign( Assign(
targets=[ targets=[
Name(id='__midas_alias_0__')], Name(id='__midas_a0__')],
value=Constant(value=123.45)), value=Constant(value=123.45)),
Assert( Assert(
test=Call( test=Call(
func=Name(id='isinstance'), func=Name(id='isinstance'),
args=[ args=[
Name(id='__midas_alias_0__'), Name(id='__midas_a0__'),
Name(id='float')], Name(id='float')],
keywords=[]), keywords=[]),
msg=JoinedStr( msg=JoinedStr(
@@ -26,7 +26,7 @@ Module(
value=Call( value=Call(
func=Name(id='type'), func=Name(id='type'),
args=[ args=[
Name(id='__midas_alias_0__')], Name(id='__midas_a0__')],
keywords=[]), keywords=[]),
attr='__name__'), attr='__name__'),
conversion=-1), conversion=-1),
@@ -34,19 +34,19 @@ Module(
Assign( Assign(
targets=[ targets=[
Name(id='distance')], Name(id='distance')],
value=Name(id='__midas_alias_0__')), value=Name(id='__midas_a0__')),
Delete( Delete(
targets=[ targets=[
Name(id='__midas_alias_0__')]), Name(id='__midas_a0__')]),
Assign( Assign(
targets=[ targets=[
Name(id='__midas_alias_1__')], Name(id='__midas_a1__')],
value=Constant(value=6.7)), value=Constant(value=6.7)),
Assert( Assert(
test=Call( test=Call(
func=Name(id='isinstance'), func=Name(id='isinstance'),
args=[ args=[
Name(id='__midas_alias_1__'), Name(id='__midas_a1__'),
Name(id='float')], Name(id='float')],
keywords=[]), keywords=[]),
msg=JoinedStr( msg=JoinedStr(
@@ -57,7 +57,7 @@ Module(
value=Call( value=Call(
func=Name(id='type'), func=Name(id='type'),
args=[ args=[
Name(id='__midas_alias_1__')], Name(id='__midas_a1__')],
keywords=[]), keywords=[]),
attr='__name__'), attr='__name__'),
conversion=-1), conversion=-1),
@@ -65,10 +65,10 @@ Module(
Assign( Assign(
targets=[ targets=[
Name(id='time')], Name(id='time')],
value=Name(id='__midas_alias_1__')), value=Name(id='__midas_a1__')),
Delete( Delete(
targets=[ targets=[
Name(id='__midas_alias_1__')]), Name(id='__midas_a1__')]),
Assign( Assign(
targets=[ targets=[
Name(id='speed')], Name(id='speed')],

View File

@@ -2582,18 +2582,21 @@
"name": "__sub__", "name": "__sub__",
"type": { "type": {
"_type": "FunctionType", "_type": "FunctionType",
"pos_args": [ "params": {
{ "_type": "ParamSpec",
"name": null, "pos": [
"type": { {
"_type": "NamedType", "name": null,
"name": "GeoLocation" "type": {
}, "_type": "NamedType",
"required": true "name": "GeoLocation"
} },
], "required": true
"args": [], }
"kw_args": [], ],
"mixed": [],
"kw": []
},
"returns": { "returns": {
"_type": "GenericType", "_type": "GenericType",
"type": { "type": {
@@ -2673,18 +2676,21 @@
"name": "__sub__", "name": "__sub__",
"type": { "type": {
"_type": "FunctionType", "_type": "FunctionType",
"pos_args": [ "params": {
{ "_type": "ParamSpec",
"name": null, "pos": [
"type": { {
"_type": "NamedType", "name": null,
"name": "Latitude" "type": {
}, "_type": "NamedType",
"required": true "name": "Latitude"
} },
], "required": true
"args": [], }
"kw_args": [], ],
"mixed": [],
"kw": []
},
"returns": { "returns": {
"_type": "GenericType", "_type": "GenericType",
"type": { "type": {
@@ -2713,18 +2719,21 @@
"name": "__sub__", "name": "__sub__",
"type": { "type": {
"_type": "FunctionType", "_type": "FunctionType",
"pos_args": [ "params": {
{ "_type": "ParamSpec",
"name": null, "pos": [
"type": { {
"_type": "NamedType", "name": null,
"name": "Longitude" "type": {
}, "_type": "NamedType",
"required": true "name": "Longitude"
} },
], "required": true
"args": [], }
"kw_args": [], ],
"mixed": [],
"kw": []
},
"returns": { "returns": {
"_type": "GenericType", "_type": "GenericType",
"type": { "type": {
@@ -2745,12 +2754,24 @@
{ {
"_type": "PredicateStmt", "_type": "PredicateStmt",
"name": "Positive", "name": "Positive",
"subject": "v", "params": [
"type": { {
"_type": "NamedType", "_type": "ParamSpec",
"name": "float" "pos": [],
}, "mixed": [
"condition": { {
"name": "v",
"type": {
"_type": "NamedType",
"name": "float"
},
"required": true
}
],
"kw": []
}
],
"body": {
"_type": "BinaryExpr", "_type": "BinaryExpr",
"left": { "left": {
"_type": "VariableExpr", "_type": "VariableExpr",
@@ -2766,12 +2787,24 @@
{ {
"_type": "PredicateStmt", "_type": "PredicateStmt",
"name": "StrictlyPositive", "name": "StrictlyPositive",
"subject": "v", "params": [
"type": { {
"_type": "NamedType", "_type": "ParamSpec",
"name": "float" "pos": [],
}, "mixed": [
"condition": { {
"name": "v",
"type": {
"_type": "NamedType",
"name": "float"
},
"required": true
}
],
"kw": []
}
],
"body": {
"_type": "BinaryExpr", "_type": "BinaryExpr",
"left": { "left": {
"_type": "VariableExpr", "_type": "VariableExpr",
@@ -2787,12 +2820,24 @@
{ {
"_type": "PredicateStmt", "_type": "PredicateStmt",
"name": "Equatorial", "name": "Equatorial",
"subject": "loc", "params": [
"type": { {
"_type": "NamedType", "_type": "ParamSpec",
"name": "GeoLocation" "pos": [],
}, "mixed": [
"condition": { {
"name": "loc",
"type": {
"_type": "NamedType",
"name": "GeoLocation"
},
"required": true
}
],
"kw": []
}
],
"body": {
"_type": "GroupingExpr", "_type": "GroupingExpr",
"expr": { "expr": {
"_type": "BinaryExpr", "_type": "BinaryExpr",
@@ -2827,12 +2872,24 @@
{ {
"_type": "PredicateStmt", "_type": "PredicateStmt",
"name": "Arctic", "name": "Arctic",
"subject": "loc", "params": [
"type": { {
"_type": "NamedType", "_type": "ParamSpec",
"name": "GeoLocation" "pos": [],
}, "mixed": [
"condition": { {
"name": "loc",
"type": {
"_type": "NamedType",
"name": "GeoLocation"
},
"required": true
}
],
"kw": []
}
],
"body": {
"_type": "GroupingExpr", "_type": "GroupingExpr",
"expr": { "expr": {
"_type": "BinaryExpr", "_type": "BinaryExpr",

View File

@@ -45,7 +45,7 @@ class GeneratorTester(Tester):
typed_ast: TypedAST = checker.type_check(path) typed_ast: TypedAST = checker.type_check(path)
if not any(d.type == DiagnosticType.ERROR for d in checker.diagnostics): if not any(d.type == DiagnosticType.ERROR for d in checker.diagnostics):
generator = Generator(workdir=path.parent) generator = Generator(workdir=path.parent, types=checker.types)
result.compiled_ast = generator.generate_ast(typed_ast, path) result.compiled_ast = generator.generate_ast(typed_ast, path)
return result return result

View File

@@ -2,6 +2,7 @@ from typing import Optional, Sequence
from midas.ast.midas import ( from midas.ast.midas import (
BinaryExpr, BinaryExpr,
CallExpr,
ComplexType, ComplexType,
ConstraintType, ConstraintType,
Expr, Expr,
@@ -15,6 +16,7 @@ from midas.ast.midas import (
LogicalExpr, LogicalExpr,
MemberStmt, MemberStmt,
NamedType, NamedType,
ParamSpec,
PredicateStmt, PredicateStmt,
Stmt, Stmt,
Type, Type,
@@ -78,9 +80,8 @@ class MidasAstJsonSerializer(
return { return {
"_type": "PredicateStmt", "_type": "PredicateStmt",
"name": stmt.name.lexeme, "name": stmt.name.lexeme,
"subject": stmt.subject.lexeme, "params": [self._serialize_param_spec(spec) for spec in stmt.params],
"type": stmt.type.accept(self), "body": stmt.body.accept(self),
"condition": stmt.condition.accept(self),
} }
def visit_logical_expr(self, expr: LogicalExpr) -> dict: def visit_logical_expr(self, expr: LogicalExpr) -> dict:
@@ -106,6 +107,14 @@ class MidasAstJsonSerializer(
"right": expr.right.accept(self), "right": expr.right.accept(self),
} }
def visit_call_expr(self, expr: CallExpr) -> dict:
return {
"_type": "CallExpr",
"callee": expr.callee.accept(self),
"arguments": self._serialize_list(expr.arguments),
"keywords": {name: arg.accept(self) for name, arg in expr.keywords.items()},
}
def visit_get_expr(self, expr: GetExpr) -> dict: def visit_get_expr(self, expr: GetExpr) -> dict:
return { return {
"_type": "GetExpr", "_type": "GetExpr",
@@ -163,15 +172,21 @@ class MidasAstJsonSerializer(
def visit_function_type(self, type: FunctionType) -> dict: def visit_function_type(self, type: FunctionType) -> dict:
return { return {
"_type": "FunctionType", "_type": "FunctionType",
"pos_args": [self._serialize_func_arg(arg) for arg in type.pos_args], "params": self._serialize_param_spec(type.params),
"args": [self._serialize_func_arg(arg) for arg in type.args],
"kw_args": [self._serialize_func_arg(arg) for arg in type.kw_args],
"returns": type.returns.accept(self), "returns": type.returns.accept(self),
} }
def _serialize_param_spec(self, spec: ParamSpec) -> dict:
return {
"_type": "ParamSpec",
"pos": [self._serialize_func_arg(arg) for arg in spec.pos],
"mixed": [self._serialize_func_arg(arg) for arg in spec.mixed],
"kw": [self._serialize_func_arg(arg) for arg in spec.kw],
}
def _serialize_func_arg(self, arg: FunctionType.Argument) -> dict: def _serialize_func_arg(self, arg: FunctionType.Argument) -> dict:
return { return {
"name": arg.name, "name": arg.name.lexeme if arg.name is not None else None,
"type": arg.type.accept(self), "type": arg.type.accept(self),
"required": arg.required, "required": arg.required,
} }