#%% basic,barebones
a = 5
b: float = 6.16
c: optional[str] = None
print a, b, c  #: 5 6.16 None

#%% late_unify,barebones
a = []
a.append(1)
print a  #: [1]
print [1]+[1]  #: [1, 1]

#%% late_unify_2,barebones
class XX[T]:
    y: T
a = XX()
def f(i: int) -> int:
    return i
print a.y.__class__.__name__ #: int
f(a.y)
print a.__class__.__name__ #: XX[int]
print XX[bool].__class__.__name__ #: XX[bool]

#%% nested_generic,barebones
x = Array[Array[int]](0)
f = Optional[Optional[Optional[int]]](Optional[Optional[int]](Optional[int](5)))
print x.len, f  #: 0 5

#%% map_unify
def map[T,S](l: List[T], f: Callable[[T], S]):
    return [f(x) for x in l]
e = 1
print map([1, 2, 3], lambda x: x+e)  #: [2, 3, 4]

def map2(l, f):
    return [f(x) for x in l]
print map2([1, 2, 3], lambda x: x+e)  #: [2, 3, 4]

#%% nested,barebones
def m4[TD](a: int, d: TD):
    def m5[TD,TE](a: int, d: TD, e: TE):
        print a, d, e
    m5(a, d, 1.12)
m4(1, 's')  #: 1 s 1.12
m4(1, True)  #: 1 True 1.12

#%% nested_class,barebones
class A[TA]:
    a: TA
    # lots of nesting:
    def m4[TD](self: A[TA], d: TD):
        def m5[TA,TD,TE](a: TA, d: TD, e: TE):
            print a, d, e
        m5(self.a, d, d)
ax = A(42)
ax.m4(1)  #: 42 1 1

#%% static_fn,barebones
class A[TA]:
    a: TA
    def dump(a, b, c):
        print a, b, c
    def m2():
        A.dump(1, 2, 's')
    def __str__(self):
        return 'A'
A.dump(1, 2, 3)  #: 1 2 3
A[int].m2()  #: 1 2 s
A.m2()  #: 1 2 s
c = A[str]('s')
c.dump('y', 1.1)  #: A y 1.1

#%% static_fn_overload,barebones
def foo(x: Static[int]):
    print('int', x)

@overload
def foo(x: Static[str]):
    print('str', x)

foo(10)
#: int 10
foo('s')
#: str s

#%% realization_big
class A[TA,TB,TC]:
    a: TA
    b: TB
    c: TC

    def dump(a, b, c):
        print a, b, c

    # non-generic method:
    def m0(self: A[TA,TB,TC], a: int):
        print a

    # basic generics:
    def m1[X](self: A[TA,TB,TC], other: A[X,X,X]):
        print other.a, other.b, other.c

    # non-generic method referencing outer generics:
    def m2(a: TA, b: TB, c: TC):
        A.dump(a, b, c)

    # generic args:
    def m3(self, other):
        return self.a

    # lots of nesting:
    def m4[TD](self: A[TA,TB,TC], d: TD):
        def m5[TA,TB,TC,TD,TE](a: TA, b: TB, c: TC, d: TD, e: TE):
            print a, b, c, d, e
        m5(self.a, self.b, self.c, d, d)

    # instantiating the type:
    def m5(self):
        x = A(self.a, self.b, self.c)
        A.dump(x.a, x.b, x.c)

    # deeply nested generic type:
    def m6[T](v: array[array[array[T]]]):
        return v[0][0][0]
a1 = A(42, 3.14, "hello")
a2 = A(1, 2, 3)
a1.m1(a2)                           #: 1 2 3
A[int,float,str].m2(1, 1.0, "one")  #: 1 1 one
A[int,int,int].m2(11, 22, 33)       #: 11 22 33
print a1.m3(a2)                     #: 42
print a1.m3(a2)                     #: 42
print a2.m3(a1)                     #: 1
a1.m4(True)                         #: 42 3.14 hello True True
a1.m4([1])                          #: 42 3.14 hello [1] [1]
a2.m4("x")                          #: 1 2 3 x x
a1.m5()                             #: 42 3.14 hello
a2.m5()                             #: 1 2 3

v1 = array[array[array[str]]](1)
v2 = array[array[str]](1)
v3 = array[str](1)
v1[0] = v2
v2[0] = v3
v3[0] = "world"
print A.m6(v1)                      #: world

f = a2.m0
f(99)                               #: 99

#%% realization_small,barebones
class B1[T]:
    a: T
    def foo[S](self: S) -> B1[int]:
        return B1[int](111)
b1 = B1[bool](True).foo()
print b1.foo().a                    #: 111

class B2[T]:
    a: T
    def foo[S](self: B2[S]):
        return B2[int](222)
b2 = B2[str]("x").foo()
print b2.foo().a                    #: 222

# explicit realization:
def m7[T,S]():
    print "works"
m7(str,float)                       #: works
m7(str,float)                       #: works
m7(float,str)                       #: works

#%% recursive,barebones
def foo(a):
    if not a:
        foo(True)
    print a
foo(0)
#: True
#: 0

def bar(a):
    def baz(x):
        if not x:
            bar(True)
        print (x)
    baz(a)
bar(0)
#: True
#: 0

def rec2(x, y):
    if x:
        return rec2(y, x)
    else:
        return 1.0
print rec2(1, False).__class__.__name__ #: float

def pq(x):
    return True
def rec3(x, y):
    if pq(x):
        return rec3(y, x)
    else:
        return y
print rec3('x', 's').__class__.__name__  #: str

# Nested mutually recursive function
def f[T](x: T) -> T:
    def g[T](z):
        return z(T())
    return g(f, T=T)
print f(1.2).__class__.__name__ #: float
print f('s').__class__.__name__ #: str

def f2[T](x: T):
    return f2(x - 1, T) if x else 1
print f2(1) #: 1
print f2(1.1).__class__.__name__ #: int


#%% recursive_error,barebones
def pq(x):
    return True
def rec3(x, y): #- ('a, 'b) -> 'b
    if pq(x):
        return rec3(y, x)
    else:
        return y
rec3(1, 's')
#! 'int' does not match expected type 'str'
#! during the realization of rec3(x: int, y: str)

#%% instantiate_function_2,barebones
def fx[T](x: T) -> T:
    def g[T](z):
        return z(T())
    return g(fx, T)
print fx(1.1).__class__.__name__, fx(1).__class__.__name__ #: float int

#%% optionals,barebones
y = None
print y  #: None
y = 5
print y  #: 5

def foo(x: optional[int], y: int):
    print 'foo', x, y
foo(y, 6)  #: foo 5 6
foo(5, 6)  #: foo 5 6
foo(5, y)  #: foo 5 5
y = None
try:
    foo(5, y)
except ValueError:
    print 'unwrap failed'  #: unwrap failed

class Cls:
    x: int
c = None
for i in range(2):
    if c: c.x += 1  # check for unwrap() dot access
    c = Cls(1)
print(c.x)  #: 1

#%% optional_methods,barebones
@extend
class int:
    def x(self):
        print 'x()!', self

y = None
z = 1 if y else None
print z  #: None

y = 6
z = 1 + y if y else None
print z  #: 7
z.x()  #: x()! 7
if 1: # otherwise compiler won't compile z.x() later
    z = None
try:
    z.x()
except ValueError:
    print 'unwrap failed'  #: unwrap failed

print Optional(1) + Optional(2)  #: 3
print Optional(1) + 3  #: 4
print 1 + Optional(1)  #: 2

#%% optional_tuple,barebones
a = None
if True:
    a = ('x', 's')
print(a)  #: ('x', 's')
print(*a, (1, *a))  #: x s (1, 'x', 's')
x,y=a
print(x,y,[*a]) #: x s ['x', 's']

#%% global_none,barebones
a, b = None, None
def foo():
    global a, b
    a = [1, 2]
    b = 3
print a, b,
foo()
print a, b #: None None [1, 2] 3

#%% default_type_none
class Test:
    value: int
    def __init__(self, value: int):
        self.value = value
    def __repr__(self):
        return str(self.value)
def key_func(k: Test):
    return k.value
print sorted([Test(1), Test(3), Test(2)], key=key_func)  #: [1, 2, 3]
print sorted([Test(1), Test(3), Test(2)], key=lambda x: x.value)  #: [1, 2, 3]
print sorted([1, 3, 2])  #: [1, 2, 3]

#%% nested_map
print list(map(lambda i: i-2, map(lambda i: i+1, range(5))))
#: [-1, 0, 1, 2, 3]

def h(x: list[int]):
    return x
print h(list(map(lambda i: i-1, map(lambda i: i+2, range(5)))))
#: [1, 2, 3, 4, 5]

#%% func_unify_error,barebones
def foo(x:int):
    print x
z = 1 & foo #! unsupported operand type(s) for &: 'int' and 'foo[int]'

#%% tuple_type_late,barebones
coords = []
for i in range(2):
    coords.append( ('c', i, []) )
coords[0][2].append((1, 's'))
print(coords)  #: [('c', 0, [(1, 's')]), ('c', 1, [])]

#%% void,barebones
def foo():
    print 'foo'
def bar(x):
    print 'bar', x.__class__.__name__
a = foo()  #: foo
bar(a)  #: bar NoneType

def x():
  pass
b = lambda: x()
b()
x() if True else x()

#%% void_2,barebones
def foo():
    i = 0
    while i < 10:
        print i  #: 0
        yield
        i += 10
a = list(foo())
print(a)  #: [None]

#%% instantiate_swap,barebones
class Foo[T, U]:
    t: T
    u: U
    def __init__(self):
        self.t = T()
        self.u = U()
    def __str__(self):
        return f'{self.t} {self.u}'
print Foo[int, bool](), Foo[bool, int]() #: 0 False False 0

#%% static,barebones
class Num[N_: Static[int]]:
    def __str__(self):
        return f'[{N_}]'
    def __init__(self):
        pass
def foo[N: Static[int]]():
    print Num[N*2]()
foo(3) #: [6]

class XX[N_: Static[int]]:
    a: Num[N_*2]
    def __init__(self):
        self.a = Num()
y = XX[5]()
print y.a, y.__class__.__name__, y.a.__class__.__name__ #: [10] XX[5] Num[10]

@tuple
class FooBar[N: Static[int]]:
    x: Int[N]
z = FooBar(i32(5))
print z, z.__class__.__name__, z.x.__class__.__name__ #: (x: Int[32](5)) FooBar[32] Int[32]

@tuple
class Foo[N: Static[int]]:
    x: Int[2*N]
    def __new__(x: Int[2*N]) -> Foo[N]:
        return (x,)
foo = Foo[10](Int[20](0))
print foo.__class__.__name__, foo.x.__class__.__name__ #: Foo[10] Int[20]

#%% static_2,barebones
class Num[N: Static[int]]:
    def __str__(self):
        return f'~{N}'
    def __init__(self):
        pass
class Foo[T, A: Static[int], B: Static[int]]:
    a: Num[A+B]
    b: Num[A-B]
    c: Num[A if A > 3 else B]
    t: T
    def __init__(self):
        self.a = Num()
        self.b = Num()
        self.c = Num()
        self.t = T()
    def __str__(self):
        return f'<{self.a} {self.b} {self.c} :: {self.t}>'
print Foo[int, 3, 4](), Foo[int, 5, 4]()
#: <~7 ~-1 ~4 :: 0> <~9 ~1 ~5 :: 0>

#%% static_int,barebones
def foo(n: Static[int]):
    print n

a: Static[int] = 5
foo(a < 1)   #: 0
foo(a <= 1)  #: 0
foo(a > 1)   #: 1
foo(a >= 1)  #: 1
foo(a == 1)  #: 0
foo(a != 1)  #: 1
foo(a and 1) #: 1
foo(a or 1)  #: 1
foo(a + 1)   #: 6
foo(a - 1)   #: 4
foo(a * 1)   #: 5
foo(a // 2)  #: 2
foo(a % 2)   #: 1
foo(a & 2)   #: 0
foo(a | 2)   #: 7
foo(a ^ 1)   #: 4

#%% static_str,barebones
class X:
    s: Static[str]
    i: Int[1 + (s == "abc")]
    def __init__(self: X[s], s: Static[str]):
        i = Int[1+(s=="abc")]()
        print s, self.s, self.i.__class__.__name__
def foo(x: Static[str], y: Static[str]):
    print x+y
z: Static[str] = "woo"
foo("he", z)  #: hewoo
X(s='lolo') #: lolo lolo Int[1]
X('abc') #: abc abc Int[2]

#%% static_getitem
print Int[staticlen("ee")].__class__.__name__ #: Int[2]

y = [1, 2]
print getattr(y, "len") #: 2
print y.len #: 2
getattr(y, 'append')(1)
print y #: [1, 2, 1]

@extend
class Dict:
    def __getitem2__(self, attr: Static[str]):
        if hasattr(self, attr):
            return getattr(self, attr)
        else:
            return self[attr]
    def __getitem1__(self, attr: Static[int]):
        return self[attr]

d = {'s': 3.19}
print d.__getitem2__('_upper_bound') #: 3
print d.__getitem2__('s') #: 3.19
e = {1: 3.33}
print e.__getitem1__(1) #: 3.33

#%% static_fail,barebones
def test(i: Int[32]):
    print int(i)
test(Int[5](1)) #! 'Int[5]' does not match expected type 'Int[32]'

#%% static_fail_2,barebones
zi = Int[32](6)
def test3[N](i: Int[N]):
    print int(i)
test3(zi) #! 'N' does not match expected type 'N'
# TODO: nicer error message!

#%% static_fail_3,barebones
zi = Int[32](6)
def test3[N: Static[int]](i: Int[N]):
    print int(i)
test3(1, int) #! expected static expression
# TODO: nicer error message!

#%% nested_fn_generic,barebones
def f(x):
    def g(y):
        return y
    return g(x)
print f(5), f('s') #: 5 s

def f2[U](x: U, y):
    def g[T, U](x: T, y: U):
        return (x, y)
    return g(y, x)
x, y = 1, 'haha'
print f2(x, y).__class__.__name__ #: Tuple[str,int]
print f2('aa', 1.1, U=str).__class__.__name__ #: Tuple[float,str]

#%% nested_fn_generic_error,barebones
def f[U](x: U, y): # ('u, 'a) -> tuple['a, 'u]
    def g[T, U](x: T, y: U): # ('t, 'u) -> tuple['t, 'u]
        return (x, y)
    return g(y, x)
print f(1.1, 1, int).__class__.__name__ #! 'float' does not match expected type 'int'

#%% fn_realization,barebones
def ff[T](x: T, y: tuple[T]):
      print ff(T=str,...).__class__.__name__ #: ff[str,Tuple[str],str]
      return x
x = ff(1, (1,))
print x, x.__class__.__name__ #: 1 int
# print f.__class__.__name__  # TODO ERRORS

def fg[T](x:T):
    def g[T](y):
        z = T()
        return z
    print fg(T=str,...).__class__.__name__  #: fg[str,str]
    print g(1, T).__class__.__name__ #: int
fg(1)
print fg(1).__class__.__name__ #: NoneType

def f[T](x: T):
    print f(x, T).__class__.__name__  #: int
    print f(x).__class__.__name__      #: int
    print f(x, int).__class__.__name__ #: int
    return x
print f(1), f(1).__class__.__name__ #: 1 int
print f(1, int).__class__.__name__ #: int

#%% fn_realization_error,barebones
def f[T](x: T):
    print f(x, int).__class__.__name__
    return x
f('s')
#! 'str' does not match expected type 'int'
#! during the realization of f(x: str, T: str)

#%% nested_class_error,barebones
class X:
    def foo(self, x):
        return x
    class Y:
        def bar(self, x):
            return x
y = X.Y()
y.foo(1) #! 'X.Y' object has no attribute 'foo'

#%% nested_deep_class,barebones
class A[T]:
    a: T
    class B[U]:
        b: U
        class C[V]:
            c: V
            def foo[W](t: V, u: V, v: V, w: W):
                return (t, u, v, w)

print A.B.C[bool].foo(W=str, ...).__class__.__name__ #: foo[bool,bool,bool,str,str]
print A.B.C.foo(1,1,1,True) #: (1, 1, 1, True)
print A.B.C.foo('x', 'x', 'x', 'x') #: ('x', 'x', 'x', 'x')
print A.B.C.foo('x', 'x', 'x', 'x') #: ('x', 'x', 'x', 'x')
print A.B.C.foo('x', 'x', 'x', 'x') #: ('x', 'x', 'x', 'x')

x = A.B.C[bool]()
print x.__class__.__name__ #: A.B.C[bool]

#%% nested_deep_class_error,barebones
class A[T]:
    a: T
    class B[U]:
        b: U
        class C[V]:
            c: V
            def foo[W](t: V, u: V, v: V, w: W):
                return (t, u, v, w)

print A.B.C[str].foo(1,1,1,True) #! 'A.B.C[str]' object has no method 'foo' with arguments (int, int, int, bool)

#%% nested_deep_class_error_2,barebones
class A[T]:
    a: T
    class B[U]:
        b: U
        class C[V]:
            c: V
            def foo[W](t: V, u: V, v: V, w: W):
                return (t, u, v, w)
print A.B[int].C[float].foo(1,1,1,True) #! 'A.B[int]' object has no attribute 'C'

#%% nested_class_function,barebones
def f(x):
    def g(y):
        return y
    a = g(1)
    b = g('s')
    c = g(x)
    return a, b, c
print f(1.1).__class__.__name__ #: Tuple[int,str,float]
print f(False).__class__.__name__ #: Tuple[int,str,bool]

class A[T]:
    a: T
    class B[U]:
        b: U
        class C[V]:
            c: V
            def f(x):
                def g(y):
                    return y
                a = g(1)
                b = g('s')
                c = g(x)
                return a, b, c
print A.B.C.f(1.1).__class__.__name__ #: Tuple[int,str,float]
print A.B.C[Optional[int]].f(False).__class__.__name__ #: Tuple[int,str,bool]

#%% rec_class_1,barebones
class A:
    y: A
    def __init__(self): pass  # necessary to prevent recursive instantiation!
x = A()
print x.__class__.__name__, x.y.__class__.__name__ #: A A

#%% rec_class_2,barebones
class A[T]:
    a: T
    b: A[T]
    c: A[str]
    def __init__(self): pass
a = A[int]()
print a.__class__.__name__, a.b.__class__.__name__, a.c.__class__.__name__, a.b.b.__class__.__name__, a.b.c.__class__.__name__
#: A[int] A[int] A[str] A[int] A[str]
print a.c.b.__class__.__name__, a.c.c.__class__.__name__, a.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.__class__.__name__
#: A[str] A[str] A[int]

#%% rec_class_3,barebones
class X:
    x: int
    rec: X
    def __init__(self): pass
    def foo(x: X, y: int):
        return y
    class Y:
        y: int
        def bar(self, y):
            print y
            return self.y
x, y = X(), X.Y()
print x.__class__.__name__, y.__class__.__name__
#: X X.Y
print X.foo(x, 4), x.foo(5)
#: 4 5
print y.bar(1), y.bar('s'), X.Y.bar(y, True)
#: 1
#: s
#: True
#: 0 0 0

#%% rec_class_4,barebones
class A[T]:
    a: T
    b: A[T]
    c: A[str]
    def __init__(self): pass
class B[T]:
    a: T
    b: A[T]
    c: B[T]
    def __init__(self): pass
    class Nest1[U]:
        n: U
    class Nest2[T, U]:
        m: T
        n: U
b = B[float]()
print b.__class__.__name__, b.a.__class__.__name__, b.b.__class__.__name__, b.c.__class__.__name__, b.c.b.c.a.__class__.__name__
#: B[float] float A[float] B[float] str

n1 = B.Nest1[int]()
print n1.n, n1.__class__.__name__, n1.n.__class__.__name__ #: 0 B.Nest1[int] int

n1: B.Nest2 = B.Nest2[float, int]()
print (n1.m, n1.n), n1.__class__.__name__, n1.m.__class__.__name__, n1.n.__class__.__name__ #: (0, 0) B.Nest2[float,int] float int

#%% func_arg_instantiate,barebones
class A[T]:
    y: T
    def foo(self, y: T):
        self.y = y
        return y
    def bar(self, y):
        return y
a = A()
print a.__class__.__name__ #: A[int]
a.y = 5
print a.__class__.__name__ #: A[int]

b = A()
print b.foo(5) #: 5
print b.__class__.__name__, b.y #: A[int] 5
print b.bar('s'), b.bar('s').__class__.__name__ #: s str
print b.bar(5), b.bar(5).__class__.__name__ #: 5 int

aa = A()
print aa.foo('s') #: s
print aa.__class__.__name__, aa.y, aa.bar(5.1).__class__.__name__ #: A[str] s float

#%% no_func_arg_instantiate_err,barebones
# TODO: allow unbound self?
class A[T]:
    y: T
    def foo(self, y): self.y = y
a = A()
a.foo(1) #! cannot typecheck the program

#%% return_deduction,barebones
def fun[T, R](x, y: T) -> R:
   	def ffi[T, R, Z](x: T, y: R, z: Z):
   		return (x, y, z)
   	yy = ffi(False, byte(2), 's', T=bool, Z=str, R=R)
   	yz = ffi(1, byte(2), 's', T=int, Z=str, R=R)
   	return byte(1)
print fun(2, 1.1, float, byte).__class__.__name__ #: byte

#%% return_auto_deduction_err,barebones
def fun[T, R](x, y: T) -> R:
   	return byte(1)
print fun(2, 1.1).__class__.__name__ #! cannot typecheck the program

#%% random
# shuffle used to fail before for some reason (sth about unbound variables)...
def foo():
    from random import shuffle
    v = list(range(10))
    shuffle(v)
    print sorted(v) #: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
foo()

#%% function_type,barebones
class F:
    f: Function[[int], int]
    g: function[[int], None]
    x: int
def foo(x: int):
    return x+1
def goo(x: int):
    print x+2
f = F(foo, goo, 2)
print f.f(f.x) #: 3
f.g(f.x) #: 4

def hoo(z):
    print z+3
f.g = hoo
f.g(f.x)  #: 5

def hai(x, y, z):
    print f'hai({x},{y},{z})'
fn = Function[[int, int, int], None](hai)
fn(1, 2, 3) #: hai(1,2,3)
print str(fn)[:12] #: <function at
z = fn(2, 3, ...)
z(3) #: hai(2,3,3)

#%% int_float,barebones
l = [1., 2, 3, 4]
print l, l.__class__.__name__ #: [1, 2, 3, 4] List[float]

#%% forward_fn,barebones
def test(name, sort, key):
    v1 = [1, 2, 3, 4]
    sp = sort(v1, key)
    print name, sp
def foo(l, f):
    return [f(i) for i in l]
test('hi', foo, lambda x: x+1) #: hi [2, 3, 4, 5]

def foof(l: List[int], x, f: Callable[[int], int]):
    return [f(i)+x for i in l]
test('qsort', foof(x=3, ...), lambda x: x+1) #: qsort [5, 6, 7, 8]

#%% class_fn_access,barebones
class X[T]:
    def foo[U](self, x: T, y: U):
        return (x+x, y+y)
y = X[X[int]]()
print y.__class__.__name__ #: X[X[int]]
print X[float].foo(U=int, ...).__class__.__name__ #: foo[X[float],float,int,int]
print X[int]().foo(1, 's') #: (2, 'ss')

#%% class_partial_access,barebones
class X[T]:
    def foo[U](self, x, y: U):
        return (x+x, y+y)
y = X[X[int]]()
# TODO: should this even be the case?
# print y.foo(U=float,...).__class__.__name__  ->  X.foo[X[X[int]],...,...]
print y.foo(1, 2.2, float) #: (2, 4.4)

#%% forward,barebones
def foo(f, x):
    f(x, type(x))
    print f.__class__.__name__
def bar[T](x):
    print x, T.__class__.__name__
foo(bar, 1)
#: 1 int
#: bar[...]
foo(bar(...), 's')
#: s str
#: bar[...]
z = bar
z('s', int)
#: s int
z(1, T=str)
#: 1 str

zz = bar(T=int,...)
zz(1)
#: 1 int

#%% forward_error,barebones
def foo(f, x):
    f(x, type(x))
    print f.__class__.__name__
def bar[T](x):
    print x, T.__class__.__name__
foo(bar(T=int,...), 1)
#! bar() takes 2 arguments (2 given)
#! during the realization of foo(f: bar[...], x: int)
# TODO fix this error message

#%% sort_partial
def foo(x, y):
    return y**x
print sorted([1,2,3,4,5], key=foo(y=2, ...))
print sorted([1,2,3,4,5], key=foo(y=-2, ...))
#: [1, 2, 3, 4, 5]
#: [5, 3, 1, 2, 4]

#%% mutually_recursive_error,barebones
def bl(x):
    return True
def frec(x, y):
    def grec(x, y):
        return frec(y, x)
    return grec(x, y) if bl(y) else 2
print frec(1, 2).__class__.__name__, frec('s', 1).__class__.__name__
#! 'NoneType' does not match expected type 'int'
#! during the realization of frec(x: int, y: int)

#%% return_fn,barebones
def retfn(a):
    def inner(b, *args, **kwargs):
        print a, b, args, kwargs
    print inner.__class__.__name__ #: inner[...,...,int,...]
    return inner(15, ...)
f = retfn(1)
print f.__class__.__name__ #: inner[int,...,int,...]
f(2,3,foo='bar') #: 1 15 (2, 3) (foo: 'bar')

#%% decorator_manual,barebones
def foo(x, *args, **kwargs):
    print x, args, kwargs
    return 1
def dec(fn, a):
    print 'decorating', fn.__class__.__name__ #: decorating foo[...,...,...]
    def inner(*args, **kwargs):
        print 'decorator', args, kwargs #: decorator (5.5, 's') (z: True)
        return fn(a, *args, **kwargs)
    return inner(...)
ff = dec(foo(...), 10)
print ff(5.5, 's', z=True)
#: 10 (5.5, 's') (z: True)
#: 1


#%% decorator,barebones
def foo(x, *args, **kwargs):
    print x, args, kwargs
    return 1
def dec(a):
    def f(fn):
        print 'decorating', fn.__class__.__name__
        def inner(*args, **kwargs):
            print 'decorator', args, kwargs
            return fn(a, *args, **kwargs)
        return inner
    return f
ff = dec(10)(foo)
print ff(5.5, 's', z=True)
#: decorating foo[...,...,...]
#: decorator (5.5, 's') (z: True)
#: 10 (5.5, 's') (z: True)
#: 1

@dec(a=5)
def zoo(e, b, *args):
    return f'zoo: {e}, {b}, {args}'
print zoo(2, 3)
print zoo('s', 3)
#: decorating zoo[...,...,...]
#: decorator (2, 3) ()
#: zoo: 5, 2, (3)
#: decorator ('s', 3) ()
#: zoo: 5, s, (3)

def mydecorator(func):
    def inner():
        print("before")
        func()
        print("after")
    return inner
@mydecorator
def foo2():
    print("foo")
foo2()
#: before
#: foo
#: after

def timeme(func):
    def inner(*args, **kwargs):
        begin = 1
        end = func(*args, **kwargs) - begin
        print('time needed for', func.__class__.__name__, 'is', end)
    return inner
@timeme
def factorial(num):
    n = 1
    for i in range(1,num + 1):
        n *= i
    print(n)
    return n
factorial(10)
#: 3628800
#: time needed for factorial[...] is 3628799

def dx1(func):
    def inner():
        x = func()
        return x * x
    return inner
def dx2(func):
    def inner():
        x = func()
        return 2 * x
    return inner
@dx1
@dx2
def num():
    return 10
print(num()) #: 400

def dy1(func):
    def inner(*a, **kw):
        x = func(*a, **kw)
        return x * x
    return inner
def dy2(func):
    def inner(*a, **kw):
        x = func(*a, **kw)
        return 2 * x
    return inner
@dy1
@dy2
def num2(a, b):
    return a+b
print(num2(10, 20)) #: 3600

#%% hetero_iter,barebones
e = (1, 2, 3, 'foo', 5, 'bar', 6)
for i in e:
    if isinstance(i, int):
        if i == 1: continue
    if isinstance(i, str):
        if i == 'bar': break
    print i

#%% type_loc,barebones
a = 1
T = type(a)
print T.__class__.__name__  #: int

#%% empty_tuple,barebones
T = type(())  # only errors with empty tuple type
p = Ptr[T](cobj())
print p.__class__.__name__  #: Ptr[Tuple]

print [a for a in ()]  #: []

def foo(*args):
    return [a for a in args]
args, result = ((), [()])
print list(foo(*args))  #: []
print result  #: [()]


#%% type_error_reporting
# TODO: improve this certainly
def tee(iterable, n=2):
    from collections import deque
    it = iter(iterable)
    deques = [deque() for i in range(n)]
    def gen(mydeque):
        while True:
            if not mydeque:             # when the local deque is empty
                if it.done():
                    return
                newval = it.next()
                for d in deques:        # load it to all the deques
                    d.append(newval)
            yield mydeque.popleft()
    return list(gen(d) for d in deques)
it = [1,2,3,4]
a, b = tee(it)
#! cannot typecheck the program
#! during the realization of tee(iterable: List[int], n: int)

#%% new_syntax,barebones
def foo[T,U](x: type, y, z: Static[int] = 10):
    print T.__class__.__name__, U.__class__.__name__, x.__class__.__name__, y.__class__.__name__, Int[z+1].__class__.__name__
    return List[x]()
print foo(T=int,U=str,...).__class__.__name__ #: foo[T1,x,z,int,str]
print foo(T=int,U=str,z=5,x=bool,...).__class__.__name__ #: foo[T1,bool,5,int,str]
print foo(float,3,T=int,U=str,z=5).__class__.__name__ #: List[float]
foo(float,1,10,str,int) #: str int float int Int[11]


class Foo[T,U: Static[int]]:
    a: T
    b: Static[int]
    c: Int[U]
    d: type
    e: List[d]
    f: UInt[b]
print Foo[5,int,float,6].__class__.__name__ #: Foo[5,int,float,6]
print Foo(1.1, 10i32, [False], 10u66).__class__.__name__ #: Foo[66,bool,float,32]


def foo2[N: Static[int]]():
    print Int[N].__class__.__name__, N
x: Static[int] = 5
y: Static[int] = 105 - x * 2
foo2(y-x) #: Int[90] 90

if 1.1+2.2 > 0:
    z: Static[int] = 88
    print z #: 88
print x #: 5
x : Static[int] = 3
print x #: 3

def fox(N: Static[int] = 4):
    print Int[N].__class__.__name__, N
fox(5) #: Int[5] 5
fox() #: Int[4] 4

#%% new_syntax_err,barebones
class Foo[T,U: Static[int]]:
    a: T
    b: Static[int]
    c: Int[U]
    d: type
    e: List[d]
    f: UInt[b]
print Foo[float,6].__class__.__name__ #! Foo takes 4 generics (2 given)

#%% partial_star_pipe_args,barebones
iter(['A', 'C']) |> print
#: A
#: C
iter(range(4)) |> print('x', ..., 1)
#: x 0 1
#: x 1 1
#: x 2 1
#: x 3 1

#%% type_arg_transform,barebones
print list(map(str, range(5)))
#: ['0', '1', '2', '3', '4']


#%% traits,barebones
def t[T](x: T, key: Optional[Callable[[T], S]] = None, S: type = NoneType):
    if isinstance(S, NoneType):
        return x
    else:
        return (key.__val__())(x)
print t(5) #: 5
print t(6, lambda x: f'#{x}') #: #6

z: Callable[[int],int] = lambda x: x+1
print z(5) #: 6

def foo[T](x: T, func: Optional[Callable[[], T]] = None) -> T:
    return x
print foo(1) #: 1

#%% trait_callable
foo = [1,2,11]
print(sorted(foo, key=str))
#: [1, 11, 2]

foo = {1: "a", 2: "a", 11: "c"}
print(sorted(foo.items(), key=str))
#: [(1, 'a'), (11, 'c'), (2, 'a')]

def call(f: Callable[[int,int], Tuple[str,int]]):
    print(f(1, 2))

def foo(*x): return f"{x}_{x.__class__.__name__}",1
call(foo)
#: ('(1, 2)_Tuple[int,int]', 1)

def foo(a:int, *b: float): return f"{a}_{b}", a+b[0]
call(foo)
#: ('1_(2)', 3)

def call(f: Callable[[int,int],str]):
    print(f(1, 2))
def foo(a: int, *b: int, **kw): return f"{a}_{b}_{kw}"
call(foo(zzz=1.1, ...))
#: 1_(2)_(zzz: 1.1)

#%% traits_error,barebones
def t[T](x: T, key: Optional[Callable[[T], S]] = None, S: type = NoneType):
    if isinstance(S, NoneType):
        return x
    else:
        return (key.__val__())(x)
print t(6, Optional(1)) #! 'Optional[int]' does not match expected type 'Optional[Callable[[int],S]]'

#%% traits_error_2,barebones
z: Callable[[int],int] = 4 #! 'Callable[[int],int]' does not match expected type 'int'

#%% assign_wrappers,barebones
a = 1.5
print a #: 1.5
if 1:
    a = 1
print a, a.__class__.__name__ #: 1 float

a: Optional[int] = None
if 1:
    a = 5
print a.__class__.__name__, a #: Optional[int] 5

b = 5
c = Optional(6)
if 1:
    b = c
print b.__class__.__name__, c.__class__.__name__, b, c #: int Optional[int] 6 6

z: Generator[int] = [1, 2]
print z.__class__.__name__ #: Generator[int]

zx: float = 1
print zx.__class__.__name__, zx #: float 1

def test(v: Optional[int]):
    v: int = v if v is not None else 3
    print v.__class__.__name__
test(5) #: int
test(None) #: int

#%% methodcaller,barebones
def foo():
    def bar(a, b):
        print 'bar', a, b
    return bar
foo()(1, 2) #: bar 1 2

def methodcaller(foo: Static[str]):
    def caller(foo: Static[str], obj, *args, **kwargs):
        if isinstance(getattr(obj, foo)(*args, **kwargs), None):
            getattr(obj, foo)(*args, **kwargs)
        else:
            return getattr(obj, foo)(*args, **kwargs)
    return caller(foo=foo, ...)
v = [1]
methodcaller('append')(v, 42)
print v #: [1, 42]
print methodcaller('index')(v, 42) #: 1

#%% fn_overloads,barebones
def foo(x):
    return 1, x

print(foo(''))  #: (1, '')

@overload
def foo(x, y):
    def foo(x, y):
        return f'{x}_{y}'
    return 2, foo(x, y)

@overload
def foo(x):
    if x == '':
        return 3, 0
    return 3, 1 + foo(x[1:])[1]

print foo('hi') #: (3, 2)
print foo('hi', 1) #: (2, 'hi_1')

#%% fn_shadow,barebones
def foo(x):
    return 1, x
print foo('hi') #: (1, 'hi')

def foo(x):
    return 2, x
print foo('hi') #: (2, 'hi')

#%% fn_overloads_error,barebones
def foo(x):
    return 1, x
@overload
def foo(x, y):
    return 2, x, y
foo('hooooooooy!', 1, 2) #! no function 'foo' with arguments (str, int, int)

#%% c_void_return,barebones
from C import seq_print(str)
x = seq_print("not ")
print x  #: not None


#%% static_for,barebones
def foo(i: Static[int]):
    print('static', i, Int[i].__class__.__name__)

for i in statictuple(1, 2, 3, 4, 5):
    foo(i)
    if i == 3: break
#: static 1 Int[1]
#: static 2 Int[2]
#: static 3 Int[3]
for i in staticrange(9, 4, -2):
    foo(i)
    if i == 3:
        break
#: static 9 Int[9]
#: static 7 Int[7]
#: static 5 Int[5]
for i in statictuple("x", 1, 3.3, 2):
    print(i)
#: x
#: 1
#: 3.3
#: 2

print tuple(Int[i+10](i) for i in statictuple(1, 2, 3)).__class__.__name__
#: Tuple[Int[11],Int[12],Int[13]]

for i in staticrange(0, 10):
    if i % 2 == 0: continue
    if i > 8: break
    print('xyz', Int[i].__class__.__name__)
print('whoa')
#: xyz Int[1]
#: xyz Int[3]
#: xyz Int[5]
#: xyz Int[7]
#: whoa

for i in staticrange(15):
    if i % 2 == 0: continue
    if i > 8: break
    print('xyz', Int[i].__class__.__name__)
print('whoa')
#: xyz Int[1]
#: xyz Int[3]
#: xyz Int[5]
#: xyz Int[7]
#: whoa

print tuple(Int[i-10](i) for i in staticrange(30,33)).__class__.__name__
#: Tuple[Int[20],Int[21],Int[22]]

for i in statictuple(0, 2, 4, 7, 11, 12, 13):
    if i % 2 == 0: continue
    if i > 8: break
    print('xyz', Int[i].__class__.__name__)
print('whoa')
#: xyz Int[7]
#: whoa

for i in staticrange(10):  # TODO: large values are too slow!
    pass
print('done')
#: done

tt = (5, 'x', 3.14, False, [1, 2])
for i, j in staticenumerate(tt):
    print(foo(i * 2 + 1), j)
#: static 1 Int[1]
#: None 5
#: static 3 Int[3]
#: None x
#: static 5 Int[5]
#: None 3.14
#: static 7 Int[7]
#: None False
#: static 9 Int[9]
#: None [1, 2]

print tuple((Int[i+1](i), j) for i, j in staticenumerate(tt)).__class__.__name__
#: Tuple[Tuple[Int[1],int],Tuple[Int[2],str],Tuple[Int[3],float],Tuple[Int[4],bool],Tuple[Int[5],List[int]]]

#%% static_range_error,barebones
for i in staticrange(1000, -2000, -2):
    pass
#! staticrange too large (expected 0..1024, got instead 1500)

#%% trait_defdict
class dd(Static[Dict[K,V]]):
    fn: S
    K: type
    V: type
    S: TypeVar[Callable[[], V]]

    def __init__(self: dd[K, VV, Function[[], V]], VV: TypeVar[V]):
        self.fn = lambda: VV()

    def __init__(self, f: S):
        self.fn = f

    def __getitem__(self, key: K) -> V:
        if key not in self:
            self.__setitem__(key, self.fn())
        return super().__getitem__(key)


x = dd(list)
x[1] = [1, 2]
print(x[2])
#: []
print(x)
#: {1: [1, 2], 2: []}

z = 5
y = dd(lambda: z+1)
y.update({'a': 5})
print(y['b'])
#: 6
z = 6
print(y['c'])
#: 7
print(y)
#: {'a': 5, 'b': 6, 'c': 7}

xx = dd(lambda: 'empty')
xx.update({1: 's', 2: 'b'})
print(xx[1], xx[44])
#: s empty
print(xx)
#: {44: 'empty', 1: 's', 2: 'b'}

s = 'mississippi'
d = dd(int)
for k in s:
    d[k] = d["x" + k]
print(sorted(d.items()))
#: [('i', 0), ('m', 0), ('p', 0), ('s', 0), ('xi', 0), ('xm', 0), ('xp', 0), ('xs', 0)]


#%% kwargs_getattr,barebones
def foo(**kwargs):
    print kwargs['foo'], kwargs['bar']

foo(foo=1, bar='s')
#: 1 s



#%% union_types,barebones
def foo_int(x: int):
    print(f'{x} {x.__class__.__name__}')
def foo_str(x: str):
    print(f'{x} {x.__class__.__name__}')
def foo(x):
    print(f'{x} {int(__internal__.union_get_tag(x))} {x.__class__.__name__}')

a: Union[int, str] = 5
foo_int(a)  #: 5 int
foo(a)  #: 5 0 Union[int,str]
print(staticlen(a))  #: 2
print(staticlen(Union[int, int]), staticlen(Tuple[int, float, int]))  #: 1 3

@extend
class str:
    def __add__(self, i: int):
        return int(self) + i

a += 6  ## this is U.__new__(a.__getter__(__add__)(59))
b = a + 59
print(a, b, a.__class__.__name__, b.__class__.__name__)  #: 11 70 Union[int,str] int

if True:
    a = 'hello'
    foo_str(a)  #: hello str
    foo(a)  #: 'hello' 1 Union[int,str]
    # b = a[1:3]
    # print(b)
print(a)  #: 'hello'

a: Union[Union[Union[str], int], Union[int, int, str]] = 9
foo(a)  #: 9 0 Union[int,str]

def ret(x):
    z : Union = x
    if x < 1: z = 1
    elif x < 10: z = False
    else: z = 'oops'
    return z
r = ret(2)
print(r, r.__class__.__name__)  #: False Union[bool,int,str]
r = ret(33.3)
print(r, r.__class__.__name__)  #: 'oops' Union[bool,float,int,str]

def ret2(x) -> Union:
    if x < 1: return 1
    elif x < 10: return 2.2
    else: return ['oops']
r = ret2(20)
print(r, r.__class__.__name__)  #: ['oops'] Union[List[str],float,int]

class A:
    x: int
    def foo(self):
        return f"A: {self.x}"
class B:
    y: str
    def foo(self):
        return f"B: {self.y}"
x : Union[A,B] = A(5)  # TODO: just Union does not work :/
print(x.foo())  #: A: 5
if True:
    x = B("bee")
print(x.foo())  #: B: bee

def do(x: A):
    print('do', x.x)
try:
    do(x)
except TypeError:
    print('error') #: error

def do2(x: B):
    print('do2', x.y)
do2(x)  #: do2 bee

z: Union[int, str] = 1
print isinstance(z, int), isinstance(z, str), isinstance(z, float), isinstance(z, Union[int, float]), isinstance(z, Union[int, str])
#: True False False False True

print isinstance(z, Union[int]), isinstance(z, Union[int, float, str])
#: False False

if True:
    z = 's'
print isinstance(z, int), isinstance(z, str), isinstance(z, float), isinstance(z, Union[int, float]), isinstance(z, Union[int, str])
#: False True False False True

class A:
    def foo(self): return 1
class B:
    def foo(self): return 's'
class C:
    def foo(self): return [True, False]
x : Union[A,B,C] = A()
print x.foo(), x.foo().__class__.__name__
#: 1 Union[List[bool],int,str]

xx = Union[int, str](0)
print(xx)  #: 0

#%% union_error,barebones
a: Union[int, str] = 123
print(123 == a)  #: True
print(a == 123)  #: True
try:
    a = "foo"
    print(a == 123)
except TypeError:
    print("oops", a)  #: oops 'foo'


#%% generator_capture_nonglobal,barebones
# Issue #49
def foo(iter):
    print(iter.__class__.__name__, list(iter))

for x in range(2):
    foo(1 for _ in range(x))
#: Generator[int] []
#: Generator[int] [1]
for x in range(2):
    for y in range(x):
        foo('z' for _ in range(y))
#: Generator[str] []

#%% nonlocal_capture_loop,barebones
# Issue #51
def kernel(fn):
    def wrapper(*args, grid, block):
        print(grid, block, fn(*args))
    return wrapper
def test_mandelbrot():
    MAX    = 10  # maximum Mandelbrot iterations
    N      = 2   # width and height of image
    pixels = [0 for _ in range(N)]
    def scale(x, a, b):
        return a + (x/N)*(b - a)
    @kernel
    def k(pixels):
        i = 0
        while i < MAX: i += 1  # this is needed for test to make sense
        return (MAX, N, pixels, scale(N, -2, 0.4))
    k(pixels, grid=(N*N)//1024, block=1024)
test_mandelbrot()  #: 0 1024 (10, 2, [0, 0], 0.4)

#%% delayed_lambda_realization,barebones
x = []
for i in range(2):
    print(all(x[j] < 0 for j in range(i)))
    x.append(i)
#: True
#: False

#%% constructor_passing
class A:
    s: str
    def __init__(self, x):
        self.s = str(x)[::-1]
    def __lt__(self, o): return self.s < o.s
    def __eq__(self, o): return self.s == o.s
    def __ge__(self, o): return self.s >= o.s
foo = [1,2,11,30]
print(sorted(foo, key=str))
#: [1, 11, 2, 30]
print(sorted(foo, key=A))
#: [30, 1, 11, 2]


#%% polymorphism,barebones
class A:
    a: int
    def foo(self, a: int): return (f'A({self.a})', a)
    def bar(self): return 'A.bar'
    def aaz(self): return 'A.aaz'
class B(A):
    b: int
    def foo(self, a): return (f'B({self.a},{self.b})', a + self.b)
    def bar(self): return 'B.bar'
    def baz(self): return 'B.baz'
class M[T]:
    m: T
    def moo(self): return (f'M_{T.__class__.__name__}', self.m)
class X(B,M[int]):
    def foo(self, a): return (f'X({self.a},{self.b},{self.m})', a + self.b + self.m)
    def bar(self): return 'X.bar'

def foo(i):
    x = i.foo(1)
    y = i.bar()
    z = i.aaz()
    print(*x, y, z)
a = A(1)
l = [a, B(2,3), X(2,3,-1)]
for i in l: foo(i)
#: A(1) 1 A.bar A.aaz
#: B(2,3) 4 B.bar A.aaz
#: X(2,3,-1) 3 X.bar A.aaz

def moo(m: M):
    print(m.moo())
moo(M[float](5.5))
moo(X(1,2,3))
#: ('M_float', 5.5)
#: ('M_int', 3)


class A[T]:
    def __init__(self):
        print("init A", T.__class__.__name__)
class Ho:
    def __init__(self):
        print("init Ho")
# TODO: this throws and error: B[U](U)
class B[U](A[U], Ho):
    def __init__(self):
        super().__init__()
        print("init B", U.__class__.__name__)
B[Ho]()
#: init A Ho
#: init B Ho


class Vehicle:
    def drive(self):
        return "I'm driving a vehicle"

class Car(Vehicle):
    def drive(self):
        return "I'm driving a car"

class Truck(Vehicle):
    def drive(self):
        return "I'm driving a truck"

class SUV(Car, Truck):
    def drive(self):
        return "I'm driving an SUV"

suv = SUV()
def moo(s):
    print(s.drive())
moo(suv)
moo(Truck())
moo(Car())
moo(Vehicle())
#: I'm driving an SUV
#: I'm driving a truck
#: I'm driving a car
#: I'm driving a vehicle


#%% polymorphism_error_1,barebones
class M[T]:
    m: T
class X(M[int]):
    pass
l = [M[float](1.1), X(2)]
#! 'List[M[float]]' object has no method 'append' with arguments (List[M[float]], X)

#%% polymorphism_2
class Expr:
    def __init__(self):
        pass
    def eval(self):
        raise ValueError('invalid expr')
        return 0.0
    def __str__(self):
        return "Expr"
class Const(Expr):
    x: float
    def __init__(self, x):
        self.x=x
    def __str__(self):
        return f"{self.x}"
    def eval(self):
        return self.x
class Add(Expr):
    lhs: Expr
    rhs: Expr
    def __init__(self, lhs, rhs):
        self.lhs=lhs
        self.rhs=rhs
        # print(f'ctr: {self}')
    def eval(self):
        return self.lhs.eval()+self.rhs.eval()
    def __str__(self):
        return f"({self.lhs}) + ({self.rhs})"
class Mul(Expr):
    lhs: Expr
    rhs: Expr
    def __init__(self, lhs, rhs):
        self.lhs=lhs
        self.rhs=rhs
    def eval(self):
        return self.lhs.eval()*self.rhs.eval()
    def __str__(self):
        return f"({self.lhs}) * ({self.rhs})"

c1 = Const(5)
c2 = Const(4)
m = Add(c1, c2)
c3 = Const(2)
a : Expr = Mul(m, c3)
print(f'{a} = {a.eval()}')
#: ((5) + (4)) * (2) = 18

from random import random, seed
seed(137)
def random_expr(depth) -> Expr:
    if depth<=0:
        return Const(int(random()*42.0))
    else:
        lhs=random_expr(depth-1)
        rhs=random_expr(depth-1)
        ctorid = int(random()*3)
        if ctorid==0:
            return Mul(lhs,rhs)
        else:
            return Add(lhs,rhs)
for i in range(11):
    print(random_expr(i).eval())
#: 17
#: 71
#: 1760
#: 118440
#: 94442
#: 8.02435e+15
#: 1.07463e+13
#: 1.43017e+19
#: 2.40292e+34
#: 6.1307e+28
#: 5.16611e+49


#%% collection_common_type,barebones
l = [1, 2, 3]
print(l, l.__class__.__name__)
#: [1, 2, 3] List[int]

l = [1.1, 2, 3]
print(l, l.__class__.__name__)
#: [1.1, 2, 3] List[float]

l = [1, 2, 3.3]
print(l, l.__class__.__name__)
#: [1, 2, 3.3] List[float]

l = [1, None]
print(l, l.__class__.__name__)
#: [1, None] List[Optional[int]]

l = [None, 2.2]
print(l, l.__class__.__name__)
#: [None, 2.2] List[Optional[float]]

class A:
    def __repr__(self): return 'A'
class B(A):
    def __repr__(self): return 'B'
class C(B):
    def __repr__(self): return 'C'
class D(A):
    def __repr__(self): return 'D'

l = [A(), B(), C(), D()]
print(l, l.__class__.__name__)
#: [A, B, C, D] List[A]

l = [D(), C(), B(), A()]
print(l, l.__class__.__name__)
#: [D, C, B, A] List[A]

l = [C(), B()]
print(l, l.__class__.__name__)
#: [C, B] List[B]

l = [C(), A(), B()]
print(l, l.__class__.__name__)
#: [C, A, B] List[A]

l = [None, *[1, 2], None]
print(l, l.__class__.__name__)
#: [None, 1, 2, None] List[Optional[int]]

# l = [C(), D(), B()] # does not work (correct behaviour)
# print(l, l.__class__.__name__)

d = {1: None, 2.2: 's'}
print(d, d.__class__.__name__)
#: {1: None, 2.2: 's'} Dict[float,Optional[str]]

#%% polymorphism_3
import operator

class Expr:
    def eval(self):
        return 0

class Const(Expr):
    value: int

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

    def eval(self):
        return self.value

class BinOp(Expr):
    lhs: Expr
    rhs: Expr

    def __init__(self, lhs, rhs):
        self.lhs = lhs
        self.rhs = rhs

    def eval_from_fn(self, fn):
        return fn(self.lhs.eval(), self.rhs.eval())

class Add(BinOp):
    def eval(self):
        return self.eval_from_fn(operator.add)

class Sub(BinOp):
    def eval(self):
        return self.eval_from_fn(operator.sub)

class Mul(BinOp):
    def eval(self):
        return self.eval_from_fn(operator.mul)

class Div(BinOp):
    def eval(self):
        return self.eval_from_fn(operator.floordiv)

# TODO: remove Expr requirement
expr : Expr = Mul(Const(3), Add(Const(10), Const(5)))
print(expr.eval()) #: 45


#%% polymorphism_4
class A(object):
    a: int
    def __init__(self, a: int):
        self.a = a

    def test_a(self, n: int):
        print("test_a:A", n)

    def test(self, n: int):
        print("test:A", n)

    def test2(self, n: int):
        print("test2:A", n)

class B(A):
    b: int
    def __init__(self, a: int, b: int):
        super().__init__(a)
        self.b = b

    def test(self, n: int):
        print("test:B", n)

    def test2(self, n: int):
        print("test2:B", n)

class C(B):
    pass

b = B(1, 2)
b.test_a(1)
b.test(1)
#: test_a:A 1
#: test:B 1

a: A = b
a.test(1)
a.test2(2)
#: test:B 1
#: test2:B 2



class AX(object):
    value: u64

    def __init__(self):
        print('init/AX')
        self.value = 15u64

    def get_value(self) -> u64:
        return self.value

class BX(object):
    a: AX
    def __init__(self):
        print('init/BX')
        self.a = AX()
    def hai(self):
        return f"hai/BX: {self.a.value}"

class CX(BX):
    def __init__(self):
        print('init/CX')
        super().__init__()

    def getsuper(self):
        return super()

    def test(self):
        print('test/CX:', self.a.value)
        return self.a.get_value()

    def hai(self):
        return f"hai/CX: {self.a.value}"

table = CX()
#: init/CX
#: init/BX
#: init/AX
print table.test()
#: test/CX: 15
#: 15

s = table.getsuper()
print(s.hai())
#: hai/BX: 15
s.a.value += 1u64
print(s.hai())
#: hai/BX: 16
table.a.value += 1u64
print(s.hai())
#: hai/BX: 17
table.test()
#: test/CX: 17

c: List[BX] = [s, table]
print(c[0].hai())  #: hai/BX: 17
print(c[1].hai())  #: hai/CX: 17



#%% no_generic,barebones
def foo(a, b: Static[int]):
    pass
foo(5)  #! generic 'b' not provided


#%% no_generic_2,barebones
def f(a, b, T: type):
    print(a, b)
f(1, 2)  #! generic 'T' not provided
