tests/base.py
NODE_EQ_PATCHER = patch.multiple(Node, __eq__=_node_eq, __ne__=_ne)
kinko/types.py
class Markup(with_metaclass(MarkupMeta, object)):
    pass
kinko/checker.py
class TypeCheckError(UserError):
    pass
kinko/checker.py
EACH_TYPE = Func[[__var.symbol, ListType[__var.item], _MarkupLike],
                 Markup]
kinko/types.py
class IntType(with_metaclass(IntTypeMeta, object)):
    pass
kinko/errors.py
WARNING = 1
kinko/checker.py
class NamesResolver(NodeTransformer):

    def __init__(self, ns):
        self.ns = ns

    def visit_symbol(self, node):
        ns, sep, name = node.name.partition('/')
        if name and ns == '.':
            return node.clone_with(sep.join([self.ns, name]))
        else:
            return node

    def visit_tuple(self, node):
        if node.values[0].name == 'def':
            (def_sym, name_sym), body = node.values[:2], node.values[2:]
            qualified_name = '/'.join([self.ns, name_sym.name])
            name_sym = name_sym.clone_with(qualified_name)
            values = ([self.visit(def_sym), name_sym] +
                      [self.visit(i) for i in body])
            return node.clone_with(values)
        else:
            return super(NamesResolver, self).visit_tuple(node)
kinko/errors.py
ERROR = 2
kinko/checker.py
HTML_TAG_TYPE = Func[[VarNamedArgs[_StringLike], VarArgs[_MarkupLike]],
                     Markup]
kinko/checker.py
GET_TYPE = Func[[Record[{}], __var.key], __var.result]
kinko/refs.py
class ArgRef(Reference):

    def __init__(self, name):
        super(ArgRef, self).__init__(None)
        self.name = name

    def __repr__(self):
        return "#{}".format(self.name)
kinko/types.py
class BoolType(with_metaclass(BoolTypeMeta, object)):
    pass
kinko/types.py
class TypeVar(with_metaclass(TypeVarMeta, object)):
    pass
kinko/checker.py
IF2_TYPE = Func[[BoolType, __var.then_, __var.else_], __var.result]
kinko/types.py
class StringType(with_metaclass(StringTypeMeta, object)):
    pass
tests/base.py
TYPE_EQ_PATCHER = patch.multiple(GenericMeta, __eq__=_type_eq, __ne__=_ne)
kinko/checker.py
DEF_TYPE = Func[[__var.name, __var.body], __var.result]
kinko/types.py
class Option(with_metaclass(OptionMeta, object)):
    pass
kinko/types.py
class ListType(with_metaclass(ListTypeMeta, object)):
    pass
kinko/nodes.py
class Tuple(Node):

    def __init__(self, values, **kw):
        self.values = tuple(values)
        super(Tuple, self).__init__(**kw)

    def __repr__(self):
        return '({})'.format(' '.join(map(repr, self.values)))

    def accept(self, visitor):
        return visitor.visit_tuple(self)
kinko/nodes.py
class Symbol(Node):

    def __init__(self, name, **kw):
        self.name = name
        head, sep, tail = name.partition('/')
        if sep:
            self.ns, self.rel = head, tail
        else:
            self.ns, self.rel = None, name
        super(Symbol, self).__init__(**kw)

    def __repr__(self):
        return self.name

    def accept(self, visitor):
        return visitor.visit_symbol(self)
kinko/types.py
class RecordMeta(TypingMeta):

    def __cls_init__(cls, items):
        cls.__items__ = dict(items)

    def __repr__(cls):
        return '{}{{{}}}'.format(
            cls.__name__,
            ' '.join(':{} {!r}'.format(*i) for i in cls.__items__.items()),
        )

    def accept(cls, visitor):
        return visitor.visit_record(cls)
kinko/types.py
class Func(with_metaclass(FuncMeta, object)):
    pass
kinko/nodes.py
class List(Node):

    def __init__(self, values, **kw):
        self.values = tuple(values)
        super(List, self).__init__(**kw)

    def __repr__(self):
        return '[{}]'.format(' '.join(map(repr, self.values)))

    def accept(self, visitor):
        return visitor.visit_list(self)
kinko/checker.py
def unify(t1, t2, backref=None):
    """Unify `t1` to match `t2`

    After unification `t1` should be equal to `t2` or `t1` would be
    a subtype of `t2`.

    `t1` may contain type variables. If `t1` comes from arguments and
    contains a record, this record will be extended. `t1`, also, may contain
    type references.

    `t2` can not contain bound type variables, only unbound type variables
    are allowed - they're represent polymorphic types in the function signature.
    `t2`, also, can not contain type references.
    """
    if isinstance(t1, TypeRefMeta):
        assert t1.__ref__ is not None, 'Unbound type reference {!r}'.format(t1)
        unify(t1.__ref__(), t2, backref)

    elif isinstance(t2, TypeRefMeta):
        assert False  # type references are not expected in t2

    elif isinstance(t2, TypeVarMeta) and t2.__instance__ is None:
        assert isinstance(backref, Reference) or backref is None, repr(backref)
        t2.__instance__ = t1
        t2.__backref__ = backref

    elif isinstance(t2, TypeVarMeta):
        assert False  # bound type-vars are not expected in t2

    elif isinstance(t1, TypeVarMeta) and t1.__instance__ is None:
        backref = t1 if t1.__backref__ else backref
        if backref and isinstance(t2, RecordMeta):
            t1.__instance__ = Record[field_refs(backref, t2.__items__)]
            unify(t1, t2, backref)
        elif backref and isinstance(t2, ListTypeMeta):
            t1.__instance__ = ListType[item_ref(backref)]
            unify(t1, t2, backref)
        elif backref and isinstance(t2, DictTypeMeta):
            raise NotImplementedError('TODO')
        else:
            t1.__instance__ = t2

    elif isinstance(t1, TypeVarMeta):
        backref = t1 if t1.__backref__ else backref
        try:
            unify(t1.__instance__, t2, backref)
        except TypeCheckError:
            if (
                backref and is_from_arg(backref) and
                not isinstance(t1.__instance__, TypingMeta) and
                isinstance(t2, type(t1.__instance__))
            ):
                t1.__instance__ = t2
            else:
                raise

    else:
        if isinstance(t1, UnionMeta):
            # all types from t1 union should unify with t2
            for t in t1.__types__:
                unify(t, t2, backref)
            return
        elif isinstance(t2, UnionMeta):
            # t1 should unify with at least one type from t2 union
            for t in t2.__types__:
                try:
                    unify(t1, t, backref)
                except TypeCheckError:
                    continue
                else:
                    return
            # not unified
        else:
            if isinstance(t1, type(t2)):
                if isinstance(t1, RecordMeta):
                    s1, s2 = set(t1.__items__), set(t2.__items__)
                    if backref and is_from_arg(backref):
                        t1.__items__.update(field_refs(backref, s2 - s1))
                    else:
                        if s2 - s1:
                            raise TypeCheckError('Missing keys {} in {!r}'
                                                 .format(s2 - s1, t1))
                    for k, v2 in t2.__items__.items():
                        unify(t1.__items__[k], v2, FieldRef(backref, k))
                    return

                elif isinstance(t1, ListTypeMeta):
                    unify(t1.__item_type__, t2.__item_type__, ItemRef(backref))
                    return

                elif isinstance(t1, DictTypeMeta):
                    unify(t1.__key_type__, t2.__key_type__, backref)
                    unify(t1.__value_type__, t2.__value_type__, backref)
                    return

                if not isinstance(t1, TypingMeta):
                    # means simple type with nullary constructor
                    return

        raise TypeCheckError('Unexpected type: {!r}, instead of: {!r}'
                             .format(t1, t2))
kinko/types.py
class DictType(with_metaclass(DictTypeMeta, object)):
    pass
kinko/types.py
class NamedArg(with_metaclass(NamedArgMeta, object)):
    pass
kinko/nodes.py
class Number(Node):

    def __init__(self, value, **kw):
        self.value = value
        super(Number, self).__init__(**kw)

    def __repr__(self):
        return repr(self.value)

    def accept(self, visitor):
        return visitor.visit_number(self)
kinko/checker.py
class _FreshVars(TypeTransformer):

    def __init__(self):
        self._mapping = {}

    def visit_typevar(self, type_):
        if type_.__instance__ is None:
            if type_ not in self._mapping:
                self._mapping[type_] = TypeVar[None]
            return self._mapping[type_]
        return self.visit(type_.__instance__)
kinko/checker.py
IF_SOME1_TYPE = Func[[__var.bind, __var.then_], __var.result]
kinko/errors.py
class UserError(Exception):
    pass
kinko/nodes.py
class Placeholder(Node):

    def __init__(self, name, **kw):
        self.name = name
        super(Placeholder, self).__init__(**kw)

    def __repr__(self):
        return '#{}'.format(self.name)

    def accept(self, visitor):
        return visitor.visit_placeholder(self)
kinko/checker.py
def restore_args(fn_type, args, norm_args, norm_args_pos):
    args_map = {}
    _norm_args = list(norm_args)
    _norm_args_pos = list(norm_args_pos)
    for arg_type in fn_type.__args__:
        if isinstance(arg_type, NamedArgMeta):
            value = _norm_args.pop(0)
            value_pos = _norm_args_pos.pop(0)
            args_map[value_pos - 1] = args[value_pos - 1]  # keyword
            args_map[value_pos] = value
        elif isinstance(arg_type, VarArgsMeta):
            values = _norm_args.pop(0)
            values_pos = _norm_args_pos.pop(0)
            for v, v_pos in zip(values, values_pos):
                args_map[v_pos] = v
        elif isinstance(arg_type, VarNamedArgsMeta):
            values_map = _norm_args.pop(0)
            values_pos = _norm_args_pos.pop(0)
            for key in values_map.keys():
                value = values_map[key]
                value_pos = values_pos[key]
                args_map[value_pos - 1] = args[value_pos - 1]  # keyword
                args_map[value_pos] = value
        else:
            value = _norm_args.pop(0)
            value_pos = _norm_args_pos.pop(0)
            args_map[value_pos] = value
    assert not _norm_args
    assert len(args_map) == len(args)
    return [args_map[i] for i in range(len(args))]
tests/test_parser.py
def parse(src):
    node = _parse(list(tokenize(src)))
    LocationChecker().visit(node)
    return node
kinko/types.py
class Record(with_metaclass(RecordMeta, object)):
    pass
kinko/nodes.py
class Keyword(Node):

    def __init__(self, name, **kw):
        self.name = name
        super(Keyword, self).__init__(**kw)

    def __repr__(self):
        return ':{}'.format(self.name)

    def accept(self, visitor):
        return visitor.visit_keyword(self)
kinko/types.py
class Union(with_metaclass(UnionMeta, object)):
    pass
kinko/types.py
class VarArgs(with_metaclass(VarArgsMeta, object)):
    pass
kinko/checker.py
def match_fn(fn_types, args):
    pos_args, kw_args = split_args(args)
    for fn_type in fn_types:
        try:
            norm_args, norm_args_pos = \
                normalize_args(fn_type, args, pos_args, kw_args)
        except SignatureMismatch:
            continue
        else:
            return fn_type, norm_args, norm_args_pos
    else:
        raise TypeCheckError('Function signature mismatch')
kinko/types.py
class VarNamedArgs(with_metaclass(VarNamedArgsMeta, object)):
    pass
kinko/checker.py
IF_SOME3_TYPE = Func[[__var.bind, NamedArg['then', __var.then_],
                      NamedArg['else', __var.else_]],
                     __var.result]
kinko/errors.py
class Errors(object):

    def __init__(self):
        self.list = []
        self._stack = [None]

    @contextmanager
    def module_ctx(self, module):
        self._stack.append(Func(module, None))
        try:
            yield
        finally:
            self._stack.pop()

    @contextmanager
    def func_ctx(self, module, name):
        self._stack.append(Func(module, name))
        try:
            yield
        finally:
            self._stack.pop()

    @contextmanager
    def location(self, location):
        try:
            yield
        except UserError as e:
            self.error(location, text_type(e))
            raise

    def warn(self, location, message):
        self.list.append(Error(self._stack[-1], location, message, WARNING))

    def error(self, location, message):
        self.list.append(Error(self._stack[-1], location, message, ERROR))
kinko/nodes.py
class String(Node):

    def __init__(self, value, **kw):
        self.value = text_type(value)
        super(String, self).__init__(**kw)

    def __repr__(self):
        return encode_basestring_ascii(self.value)

    def accept(self, visitor):
        return visitor.visit_string(self)
kinko/checker.py
def def_types(node):
    assert isinstance(node, List), type(node)
    return {d.values[1].name: Unchecked(d, False)
            for d in node.values if isinstance(d.values[1], Symbol)}
kinko/checker.py
IF1_TYPE = Func[[BoolType, __var.then_], __var.result]
kinko/checker.py
IF_SOME2_TYPE = Func[[__var.bind, __var.then_, __var.else_], __var.result]
kinko/checker.py
LET_TYPE = Func[[__var.bindings, __var.expr], __var.result]
kinko/checker.py
def check(node, env):
    if isinstance(node, Tuple):
        return check_expr(node, env)

    elif isinstance(node, Symbol):
        try:
            t = env[node.name]
        except KeyError:
            with env.errors.location(node.location):
                raise TypeCheckError('Undefined variable {}'
                                     .format(node.name))
        else:
            return node.clone_with(node.name, type=t)

    elif isinstance(node, Placeholder):
        return node.clone_with(node.name, type=env[node.name])

    elif isinstance(node, String):
        return node.clone_with(node.value, type=StringType)

    elif isinstance(node, Number):
        return node.clone_with(node.value, type=IntType)

    elif isinstance(node, List):
        values = [check(v, env) for v in node.values]
        type_ = ListType[Union[(v.__type__ for v in values)]]
        return node.clone_with(values, type=type_)

    raise NotImplementedError(repr(node))
tests/test_parser.py
class LocationChecker(NodeVisitor):

    def visit(self, node):
        assert node.location
        super(LocationChecker, self).visit(node)
kinko/checker.py
class Environ(object):

    def __init__(self, defs=None, errors=None):
        self.defs = defs or {}
        self.vars = deque([{}])
        self._root = TypeVar[None]
        self.errors = Errors() if errors is None else errors

    @contextmanager
    def push(self, mapping):
        self.vars.append(mapping)
        try:
            yield
        finally:
            self.vars.pop()

    def __getitem__(self, key):
        for d in reversed(self.vars):
            try:
                return d[key]
            except KeyError:
                continue
        else:
            type_ = self.defs[key]
            if isinstance(type_, FuncMeta):
                return type_
            else:
                var = TypeVar[type_]
                var.__backref__ = CtxRef(key)
                return var

    def __contains__(self, key):
        return any(key in d for d in self.vars) or key in self.defs

    def define(self, name, value):
        self.defs[name] = value
kinko/checker.py
IF3_TYPE = Func[[BoolType, NamedArg['then', __var.then_],
                 NamedArg['else', __var.else_]],
                __var.result]


Question: from textwrap import dedent
from kinko.refs import ArgRef
from kinko.nodes import Tuple, Symbol, Number, Keyword, List, Placeholder
from kinko.nodes import String
from kinko.types import Func, IntType, StringType, NamedArg, TypeVar, Markup
from kinko.types import Record, ListType, Union, DictType, Option
from kinko.types import VarArgs, VarNamedArgs, BoolType, RecordMeta
from kinko.errors import Errors, UserError, ERROR, WARNING
from kinko.checker import Environ, check, TypeCheckError, EACH_TYPE, _FreshVars
from kinko.checker import LET_TYPE, DEF_TYPE, GET_TYPE, IF2_TYPE, IF_SOME1_TYPE
from kinko.checker import unify, NamesResolver, def_types, IF1_TYPE, IF3_TYPE
from kinko.checker import match_fn, restore_args, HTML_TAG_TYPE, IF_SOME2_TYPE
from kinko.checker import IF_SOME3_TYPE
from .base import NODE_EQ_PATCHER, TYPE_EQ_PATCHER
from .test_parser import parse, LocationChecker
import py.test
    check_eq(b, Record[{
        'a': BoolType, 'b': IntType, 'c': IntType,
    }])


def test_unify_union():
    a = Union[StringType, IntType]
    unify(a, a)
    with py.test.raises(TypeCheckError):
        unify(a, StringType)
    unify(IntType, a)
    unify(StringType, a)


def test_unify_record():
    rec_type = Record[{'a': TypeVar[None]}]
    unify(rec_type, Record[{'a': IntType}])
    check_eq(rec_type, Record[{'a': IntType}])

    with py.test.raises(TypeCheckError):
        unify(Record[{'a': IntType}], Record[{'a': StringType}])


def test_unify_list():
    list_type = ListType[TypeVar[None]]
    unify(list_type, ListType[IntType])
    check_eq(list_type.__item_type__.__instance__, IntType)


def test_unify_dict():
    dict_type = DictType[TypeVar[None], TypeVar[None]]
    unify(dict_type, DictType[StringType, IntType])
    check_eq(dict_type.__key_type__.__instance__, StringType)
    check_eq(dict_type.__value_type__.__instance__, IntType)


def test_func():
    inc_type = Func[[IntType], IntType]
    check_expr_type(
        'inc 1',
        Tuple.typed(IntType, [Symbol.typed(inc_type, 'inc'),
                              Number.typed(IntType, 1)]),
        {'inc': inc_type},
    )

    inc_step_type = Func[[IntType, NamedArg['step', IntType]], IntType]
    check_expr_type(
        'inc-step 1 :step 2',
        Tuple.typed(IntType, [Symbol.typed(inc_step_type, 'inc-step'),
                              Number.typed(IntType, 1),
                              Keyword('step'), Number.typed(IntType, 2)]),
        {'inc-step': inc_step_type},
    )
    with py.test.raises(TypeCheckError):
        check_expr('inc "foo"', {'inc': inc_type})


def test_env_var():
    inc_type = Func[[IntType], IntType]
    check_expr_type(
        """
        inc var
        """,
        Tuple.typed(IntType, [
            Symbol.typed(inc_type, 'inc'),
            Symbol.typed(IntType, 'var'),
        ]),
        {'inc': inc_type, 'var': IntType},
    )


def test_let():
    inc_type = Func[[IntType], IntType]
    check_expr_type(
        'let [x 1] (inc x)',
        Tuple.typed(IntType, [
            Symbol.typed(LET_TYPE, 'let'),
            List([
                Symbol.typed(IntType, 'x'),
                Number.typed(IntType, 1),
            ]),
            Tuple.typed(IntType, [
                Symbol.typed(inc_type, 'inc'),
                Symbol.typed(IntType, 'x'),
            ]),
        ]),
        {'inc': inc_type},
    )


def test_infer():
    inc_type = Func[[IntType], IntType]
    foo_type = Func[[NamedArg['arg', IntType]], IntType]
    check_expr_type(
        """
        def foo
          inc #arg
        """,
        Tuple.typed(foo_type, [
            Symbol.typed(DEF_TYPE, 'def'),
            Symbol('foo'),
            Tuple.typed(IntType, [
                Symbol.typed(inc_type, 'inc'),
                Placeholder.typed(IntType, 'arg'),
            ])
        ]),
        {'inc': inc_type},
    )


def test_record():
    inc_type = Func[[IntType], IntType]
    bar_type = Record[{'baz': IntType}]
    check_expr_type(
        """
        inc bar.baz
        """,
        Tuple.typed(IntType, [
            Symbol.typed(inc_type, 'inc'),
            Tuple.typed(IntType, [

Answer:                 Symbol.typed(GET_TYPE, 'get'),