Commit ed9984de authored by Tom Niget's avatar Tom Niget

Webserver finally works

parent 048b6744
...@@ -71,12 +71,12 @@ class TyNone {}; ...@@ -71,12 +71,12 @@ class TyNone {};
template <typename T> template <typename T>
concept PyIterator = requires(T t) { concept PyIterator = requires(T t) {
{ t.py_next() } -> std::same_as<std::optional<T>>; { t.py_next() } -> std::same_as<std::optional<T>>;
}; };
template <typename T> template <typename T>
concept PyIterable = requires(T t) { concept PyIterable = requires(T t) {
{ t.py_iter() } -> PyIterator; { t.py_iter() } -> PyIterator;
}; };
template <PyIterable T, PyIterator U> U iter(const T &t) { return t.py_iter(); } template <PyIterable T, PyIterator U> U iter(const T &t) { return t.py_iter(); }
...@@ -98,14 +98,14 @@ auto len(const T &t) { ...@@ -98,14 +98,14 @@ auto len(const T &t) {
template <PyLen T> size_t len(const T &t) { return t.py_len(); }*/ template <PyLen T> size_t len(const T &t) { return t.py_len(); }*/
template<typename T> auto len(T&& t) { return dot(std::forward<T>(t), oo__len__oo)(); } template <typename T> auto len(T &&t) {
return dot(std::forward<T>(t), oo__len__oo)();
}
template <typename T> template <typename T>
concept PyNext = requires(T t) { concept PyNext = requires(T t) {
{ { t.py_next() } -> std::same_as<std::optional<typename T::value_type>>;
t.py_next() };
} -> std::same_as<std::optional<typename T::value_type>>;
};
struct { struct {
template <PyNext T> template <PyNext T>
...@@ -131,6 +131,7 @@ static constexpr auto PyNone = std::nullopt; ...@@ -131,6 +131,7 @@ static constexpr auto PyNone = std::nullopt;
// #include "builtins/complex.hpp" // #include "builtins/complex.hpp"
#include "builtins/bool.hpp" #include "builtins/bool.hpp"
#include "builtins/bytes.hpp"
#include "builtins/dict.hpp" #include "builtins/dict.hpp"
#include "builtins/int.hpp" #include "builtins/int.hpp"
#include "builtins/list.hpp" #include "builtins/list.hpp"
...@@ -179,7 +180,7 @@ struct TyFile__oo : classtype<_Base0, TyFile__oo<>> { ...@@ -179,7 +180,7 @@ struct TyFile__oo : classtype<_Base0, TyFile__oo<>> {
Task<void> operator()(auto self) { co_await typon::io::fsync(self->fd); } Task<void> operator()(auto self) { co_await typon::io::fsync(self->fd); }
} static constexpr flush{}; } static constexpr flush{};
struct Obj : value<TyStr__oo<>, Obj> { struct Obj : instance<TyFile__oo<>, Obj> {
Obj(int fd = -1, size_t len = 0) : fd(fd), len(len) {} Obj(int fd = -1, size_t len = 0) : fd(fd), len(len) {}
Obj(const Obj &other) : fd(other.fd), len(other.len) {} Obj(const Obj &other) : fd(other.fd), len(other.len) {}
...@@ -249,7 +250,7 @@ typon::Task<typon::TyFile__oo<>::Obj> open(auto path, std::string_view mode) { ...@@ -249,7 +250,7 @@ typon::Task<typon::TyFile__oo<>::Obj> open(auto path, std::string_view mode) {
int fd = co_await typon::io::openat(AT_FDCWD, path_c, flags, 0666); int fd = co_await typon::io::openat(AT_FDCWD, path_c, flags, 0666);
if (fd < 0) { if (fd < 0) {
std::cerr << path << "," << flags << std::endl; std::cerr << path_c << "," << flags << std::endl;
system_error(-fd, "openat()"); system_error(-fd, "openat()");
} }
co_return typon::TyFile__oo<>::Obj(fd, len); co_return typon::TyFile__oo<>::Obj(fd, len);
...@@ -270,8 +271,8 @@ template <typename Ref> struct lvalue_or_rvalue { ...@@ -270,8 +271,8 @@ template <typename Ref> struct lvalue_or_rvalue {
template <typename Arg> template <typename Arg>
constexpr lvalue_or_rvalue(Arg &&arg) noexcept : ref(std::move(arg)) {} constexpr lvalue_or_rvalue(Arg &&arg) noexcept : ref(std::move(arg)) {}
constexpr operator Ref &() const &noexcept { return ref; } constexpr operator Ref &() const & noexcept { return ref; }
constexpr operator Ref &&() const &&noexcept { return std::move(ref); } constexpr operator Ref &&() const && noexcept { return std::move(ref); }
constexpr Ref &operator*() const noexcept { return ref; } constexpr Ref &operator*() const noexcept { return ref; }
constexpr Ref *operator->() const noexcept { return &ref; } constexpr Ref *operator->() const noexcept { return &ref; }
}; };
...@@ -387,7 +388,7 @@ using InterpGuard = py::scoped_interpreter; ...@@ -387,7 +388,7 @@ using InterpGuard = py::scoped_interpreter;
template <typename T> template <typename T>
concept HasSync = requires(T t) { concept HasSync = requires(T t) {
{ t.sync() } -> std::same_as<T>; { t.sync() } -> std::same_as<T>;
}; };
/*auto call_sync(auto f, auto... args) { /*auto call_sync(auto f, auto... args) {
if constexpr (HasSync<decltype(f)>) { if constexpr (HasSync<decltype(f)>) {
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
#ifndef TYPON_BYTES_H #ifndef TYPON_BYTES_H
#define TYPON_BYTES_H #define TYPON_BYTES_H
#include "str.hpp"
/*class TyStr; /*class TyStr;
class TyBytes : public std::string { class TyBytes : public std::string {
...@@ -35,6 +37,12 @@ struct TyBytes__oo : classtype<_Base0, TyBytes__oo<>> { ...@@ -35,6 +37,12 @@ struct TyBytes__oo : classtype<_Base0, TyBytes__oo<>> {
} }
} static constexpr oo__add__oo{}; } static constexpr oo__add__oo{};
struct : method {
auto operator()(auto self, auto encoding) const {
return typon::TyStr(dot(self, value));
}
} static constexpr decode{};
struct Obj : value<TyBytes__oo<>, Obj> { struct Obj : value<TyBytes__oo<>, Obj> {
std::string value; std::string value;
......
...@@ -11,7 +11,6 @@ ...@@ -11,7 +11,6 @@
using namespace std::literals; using namespace std::literals;
#include "bytes.hpp"
#include "slice.hpp" #include "slice.hpp"
// #include <format> // #include <format>
#include <fmt/format.h> #include <fmt/format.h>
...@@ -177,12 +176,34 @@ struct TyStr__oo : classtype<_Base0, TyStr__oo<>> { ...@@ -177,12 +176,34 @@ struct TyStr__oo : classtype<_Base0, TyStr__oo<>> {
auto operator()(auto self) const { return (self->value.size()); } auto operator()(auto self) const { return (self->value.size()); }
} static constexpr oo__len__oo{}; } static constexpr oo__len__oo{};
/*struct : method {
auto operator()(auto self, auto other) const {
auto pos = self->value.find(other->value);
return typon::TyInt(pos == std::string::npos ? -1 : pos);
}
} static constexpr oo__find__oo{};*/
// format
/*
METHOD_GEN((typename... T), TyStr, format, (Self self, T &&...args), {
return TyStr(fmt::format(fmt::runtime(self),
std::forward<T>(args)...));
})
*/
struct : method {
auto operator()(auto self, auto... args) const {
return Obj(fmt::format(fmt::runtime(self->value), args...));
}
} static constexpr format{};
struct Obj : value<TyStr__oo<>, Obj> { struct Obj : value<TyStr__oo<>, Obj> {
std::string value; std::string value;
constexpr Obj() : value() {} constexpr Obj() : value() {}
constexpr Obj(std::string value) : value(value) {} constexpr Obj(std::string value) : value(value) {}
operator std::string() const { return value; } operator std::string() const { return value; }
operator std::string_view() const { return value; }
}; };
auto operator()(std::string value) const { return Obj(value); } auto operator()(std::string value) const { return Obj(value); }
......
...@@ -147,9 +147,10 @@ concept instance = std::derived_from< ...@@ -147,9 +147,10 @@ concept instance = std::derived_from<
referencemodel::instance<typename unwrap_all<T>::type, unwrap_all<T>>>; referencemodel::instance<typename unwrap_all<T>::type, unwrap_all<T>>>;
template <typename T> template <typename T>
concept classtype = ! concept classtype =
instance<T> &&std::derived_from< !instance<T> &&
unwrap_all<T>, referencemodel::classtype<typename unwrap_all<T>::base, std::derived_from<unwrap_all<T>,
referencemodel::classtype<typename unwrap_all<T>::base,
typename unwrap_all<T>::type>>; typename unwrap_all<T>::type>>;
template <typename F> template <typename F>
...@@ -200,8 +201,7 @@ template <typename T> ...@@ -200,8 +201,7 @@ template <typename T>
concept unwrapped = std::is_same_v<T, unwrap_one<T>>; concept unwrapped = std::is_same_v<T, unwrap_one<T>>;
template <typename T> template <typename T>
concept wrapped = ! concept wrapped = !unwrapped<T>;
unwrapped<T>;
/* Meta-programming utilities: wrapped_by */ /* Meta-programming utilities: wrapped_by */
...@@ -773,8 +773,8 @@ template <meta::wrapped T> Ref(Ref<T> &&) -> Ref<meta::unwrap_all<T>>; ...@@ -773,8 +773,8 @@ template <meta::wrapped T> Ref(Ref<T> &&) -> Ref<meta::unwrap_all<T>>;
/* Equality */ /* Equality */
template <typename T, typename U> template <typename T, typename U>
requires meta::wrapped<T> || meta::wrapped<U> bool requires meta::wrapped<T> || meta::wrapped<U>
operator==(const T &t, const U &u) { bool operator==(const T &t, const U &u) {
return t.operator->() == u.operator->(); return t.operator->() == u.operator->();
} }
...@@ -1079,7 +1079,8 @@ namespace meta { ...@@ -1079,7 +1079,8 @@ namespace meta {
} \ } \
template <meta::object Left, meta::object Right> \ template <meta::object Left, meta::object Right> \
requires meta::NormalOrAug \ requires meta::NormalOrAug \
##DUNDER##able<Left, Right> auto operator OP##=(Left &&left, Right &&right) { \ ##DUNDER##able<Left, Right> auto operator OP##=(Left &&left, \
Right &&right) { \
if constexpr (meta::Aug##DUNDER##able<Left, Right>) { \ if constexpr (meta::Aug##DUNDER##able<Left, Right>) { \
return dot(std::forward<Left>(left), \ return dot(std::forward<Left>(left), \
oo__i##DUNDER##__oo)(std::forward<Right>(right)); \ oo__i##DUNDER##__oo)(std::forward<Right>(right)); \
...@@ -1165,7 +1166,7 @@ concept DUNDERsetitemable = ...@@ -1165,7 +1166,7 @@ concept DUNDERsetitemable =
requires(Left left, Right right) { left->oo__setitem__oo(left, right); }; requires(Left left, Right right) { left->oo__setitem__oo(left, right); };
} // namespace meta } // namespace meta
/* template <meta::object Left, meta::object Right> /* template <meta::object Left, meta::object Right>
requires meta::DUNDERgetitemable<Left, Right> auto operator [](Left &&left, requires meta::DUNDERgetitemable<Left, Right> auto operator [](Left &&left,
Right &&right) { return dot(std::forward<Left>(left), Right &&right) { return dot(std::forward<Left>(left),
oo__getitem__oo)(std::forward<Right>(right)); oo__getitem__oo)(std::forward<Right>(right));
......
...@@ -91,6 +91,26 @@ struct socket__oo : referencemodel::moduletype<socket__oo<>> { ...@@ -91,6 +91,26 @@ struct socket__oo : referencemodel::moduletype<socket__oo<>> {
} }
} static constexpr close{}; } static constexpr close{};
struct : referencemodel::method {
typon::Task<typon::TyBytes__oo<>::Obj> operator()(auto self,
int bufsize) const {
std::string buf(bufsize, '\0');
co_await typon::io::recv(self->fd, buf.data(), buf.size(), 0);
co_return typon::TyBytes(std::move(buf));
}
} static constexpr recv{};
struct : referencemodel::method {
typon::Task<typon::TyNone> operator()(auto self, auto data) const {
if (int sbytes = co_await typon::io::send(self->fd, data->value, 0);
sbytes < 0) {
co_await dot(self, close)();
system_error(-sbytes, "send()");
}
co_return {};
}
} static constexpr send{};
/*METHOD(typon::Task<void>, close, (Self self), /*METHOD(typon::Task<void>, close, (Self self),
{ co_await typon::io::close(self->fd); }) { co_await typon::io::close(self->fd); })
......
...@@ -94,7 +94,7 @@ assert(len(["a"])) ...@@ -94,7 +94,7 @@ assert(len(["a"]))
#assert list.__getitem__ #assert list.__getitem__
assert [].__getitem__ assert [].__getitem__
assert [4].__getitem__ assert [4].__getitem__
assert [1, 2, 3][1] assert [1, 2, 3].__getitem__(1)
class set[U]: class set[U]:
def __len__(self) -> int: ... def __len__(self) -> int: ...
...@@ -122,7 +122,7 @@ def identity_2[U, V](x: U, y: V) -> tuple[U, V]: ...@@ -122,7 +122,7 @@ def identity_2[U, V](x: U, y: V) -> tuple[U, V]:
... ...
assert list.__add__ assert list.__add__
assert list.__add__([5], [[6][0]]) assert list.__add__([5], [6])
assert list[int].__add__ assert list[int].__add__
assert identity_2(1, "a") assert identity_2(1, "a")
assert lambda x, y: identity_2(x, y) assert lambda x, y: identity_2(x, y)
......
...@@ -26,7 +26,7 @@ class socket: ...@@ -26,7 +26,7 @@ class socket:
def recv(self, bufsize: int) -> Task[bytes]: def recv(self, bufsize: int) -> Task[bytes]:
pass pass
def send(self, data: bytes) -> Task[None]: def send(self, data: str) -> Task[None]:
pass pass
def __init__(self, family: int, type: int) -> Self: def __init__(self, family: int, type: int) -> Self:
......
...@@ -26,7 +26,7 @@ def read_file(path): ...@@ -26,7 +26,7 @@ def read_file(path):
def handle_connection(connfd, filepath): def handle_connection(connfd, filepath):
buf = connfd.recv(1024).decode("utf-8") buf = connfd.recv(1024).decode("utf-8")
length = buf.find("\r\n\r\n") #length = buf.find("\r\n\r\n")
content = read_file(filepath) content = read_file(filepath)
response_fmt = \ response_fmt = \
"HTTP/1.0 200 OK\r\n" \ "HTTP/1.0 200 OK\r\n" \
...@@ -35,14 +35,14 @@ def handle_connection(connfd, filepath): ...@@ -35,14 +35,14 @@ def handle_connection(connfd, filepath):
"\r\n" \ "\r\n" \
"{}" "{}"
response = response_fmt.format(len(content), content) response = response_fmt.format(len(content), content)
connfd.send(response.encode("utf-8")) connfd.send(response)
connfd.close() connfd.close()
def server_loop(sockfd, filepath): def server_loop(sockfd, filepath):
while True: while True:
x = sockfd.accept() connfd = sockfd.accept()[0]
#fork(lambda: handle_connection(connfd, filepath)) fork(lambda: handle_connection(connfd, filepath))
if __name__ == "__main__": if __name__ == "__main__":
PORT = 8000 PORT = 8000
......
...@@ -9,7 +9,7 @@ class DesugarSubscript(ast.NodeTransformer): ...@@ -9,7 +9,7 @@ class DesugarSubscript(ast.NodeTransformer):
def visit_Subscript(self, node: ast.Subscript): def visit_Subscript(self, node: ast.Subscript):
match node.ctx: match node.ctx:
case ast.Load(): case ast.Load():
return ast.Call( res = ast.Call(
func=ast.Attribute( func=ast.Attribute(
value=node.value, value=node.value,
attr="__getitem__", attr="__getitem__",
...@@ -23,3 +23,5 @@ class DesugarSubscript(ast.NodeTransformer): ...@@ -23,3 +23,5 @@ class DesugarSubscript(ast.NodeTransformer):
raise NotImplementedError("Subscript assignment and deletion not supported") raise NotImplementedError("Subscript assignment and deletion not supported")
case _: case _:
raise ValueError(f"Unexpected context {node.ctx!r}", linenodata(node)) raise ValueError(f"Unexpected context {node.ctx!r}", linenodata(node))
res.orig_node = node
return res
\ No newline at end of file
...@@ -5,7 +5,7 @@ from typing import Iterable ...@@ -5,7 +5,7 @@ from typing import Iterable
from transpiler.phases.emit_cpp.visitors import NodeVisitor, CoroutineMode, join from transpiler.phases.emit_cpp.visitors import NodeVisitor, CoroutineMode, join
from transpiler.phases.typing.scope import Scope from transpiler.phases.typing.scope import Scope
from transpiler.phases.typing.types import ClassTypeType from transpiler.phases.typing.types import ClassTypeType, TupleInstanceType
from transpiler.phases.utils import make_lnd from transpiler.phases.utils import make_lnd
from transpiler.utils import linenodata from transpiler.utils import linenodata
...@@ -300,11 +300,25 @@ class ExpressionVisitor(NodeVisitor): ...@@ -300,11 +300,25 @@ class ExpressionVisitor(NodeVisitor):
if isinstance(node.type, ClassTypeType): if isinstance(node.type, ClassTypeType):
yield from self.visit_BaseType(node.type.inner_type) yield from self.visit_BaseType(node.type.inner_type)
return return
yield "(" if isinstance(node.value.type, TupleInstanceType):
if not (isinstance(node.slice, ast.Constant) and isinstance(node.slice.value, int)):
raise NotImplementedError("Tuple subscript with non-constant not handled yet")
yield "std::get<"
yield str(node.slice.value)
yield ">("
yield from self.visit(node.value) yield from self.visit(node.value)
yield ")[" yield ")"
return
# yield "("
# yield from self.visit(node.value)
# yield ")["
# yield from self.visit(node.slice)
# yield "]"
yield "(co_await dot("
yield from self.visit(node.value)
yield ", oo__getitem__oo)("
yield from self.visit(node.slice) yield from self.visit(node.slice)
yield "]" yield "))"
def visit_UnaryOp(self, node: ast.UnaryOp) -> Iterable[str]: def visit_UnaryOp(self, node: ast.UnaryOp) -> Iterable[str]:
yield from self.visit_unary_operation(node.op, node.operand) yield from self.visit_unary_operation(node.op, node.operand)
......
...@@ -9,7 +9,7 @@ from transpiler.phases.typing.exceptions import ArgumentCountMismatchError, Type ...@@ -9,7 +9,7 @@ from transpiler.phases.typing.exceptions import ArgumentCountMismatchError, Type
from transpiler.phases.typing.types import BaseType, TY_STR, TY_BOOL, TY_INT, TY_COMPLEX, TY_FLOAT, TY_NONE, \ from transpiler.phases.typing.types import BaseType, TY_STR, TY_BOOL, TY_INT, TY_COMPLEX, TY_FLOAT, TY_NONE, \
ClassTypeType, ResolvedConcreteType, GenericType, CallableInstanceType, TY_LIST, TY_SET, TY_DICT, RuntimeValue, \ ClassTypeType, ResolvedConcreteType, GenericType, CallableInstanceType, TY_LIST, TY_SET, TY_DICT, RuntimeValue, \
TypeVariable, TY_LAMBDA, TypeListType, MethodType, TY_TUPLE, GenericInstanceType, PROMISES, TRANSPARENT_PROMISES, \ TypeVariable, TY_LAMBDA, TypeListType, MethodType, TY_TUPLE, GenericInstanceType, PROMISES, TRANSPARENT_PROMISES, \
TY_FORKED, TY_JOIN TY_FORKED, TY_JOIN, TypeTupleType, TupleInstanceType
from transpiler.phases.typing.scope import ScopeKind, VarDecl, VarKind from transpiler.phases.typing.scope import ScopeKind, VarDecl, VarKind
from transpiler.utils import linenodata from transpiler.utils import linenodata
...@@ -38,7 +38,17 @@ DUNDER = { ...@@ -38,7 +38,17 @@ DUNDER = {
} }
class ScoperExprVisitor(ScoperVisitor): class ScoperExprVisitor(ScoperVisitor):
def visit(self, node) -> BaseType: def visit(self, node):
res = self.visit_inner(node)
if self.scope.function and isinstance(res, GenericInstanceType) and res.generic_parent is TY_FORKED:
fty = self.scope.function.obj_type.return_type
if fty.generic_parent in PROMISES:
fty = fty.generic_args[0] # todo: check if this whole if-block works
self.scope.function.obj_type.return_type = TY_JOIN.instantiate([fty])
return res
def visit_inner(self, node) -> BaseType:
if existing := getattr(node, "type", None): if existing := getattr(node, "type", None):
return existing.resolve() return existing.resolve()
__TB_SKIP__ = True __TB_SKIP__ = True
...@@ -50,6 +60,8 @@ class ScoperExprVisitor(ScoperVisitor): ...@@ -50,6 +60,8 @@ class ScoperExprVisitor(ScoperVisitor):
if True or not hasattr(res, "from_node"): if True or not hasattr(res, "from_node"):
res.from_node = node res.from_node = node
node.type = res node.type = res
if orig := getattr(node, "orig_node", None):
orig.type = res
return res return res
def visit_Tuple(self, node: ast.Tuple) -> BaseType: def visit_Tuple(self, node: ast.Tuple) -> BaseType:
...@@ -108,6 +120,13 @@ class ScoperExprVisitor(ScoperVisitor): ...@@ -108,6 +120,13 @@ class ScoperExprVisitor(ScoperVisitor):
return TY_BOOL return TY_BOOL
def visit_Call(self, node: ast.Call) -> BaseType: def visit_Call(self, node: ast.Call) -> BaseType:
if orig := getattr(node, "orig_node", None):
if isinstance(orig, ast.Subscript):
left = self.visit(orig.value)
if isinstance(left, TupleInstanceType):
if not (isinstance(orig.slice, ast.Constant) and isinstance(orig.slice.value, int)):
raise NotImplementedError("Tuple subscript with non-constant not handled yet")
return left.fields[orig.slice.value]
ftype = self.visit(node.func) ftype = self.visit(node.func)
from transpiler.exceptions import CompileError from transpiler.exceptions import CompileError
rtype = self.visit_function_call(ftype, [self.visit(arg) for arg in node.args]) rtype = self.visit_function_call(ftype, [self.visit(arg) for arg in node.args])
...@@ -118,12 +137,6 @@ class ScoperExprVisitor(ScoperVisitor): ...@@ -118,12 +137,6 @@ class ScoperExprVisitor(ScoperVisitor):
if actual.generic_parent in TRANSPARENT_PROMISES: if actual.generic_parent in TRANSPARENT_PROMISES:
actual = actual.generic_args[0].resolve() actual = actual.generic_args[0].resolve()
if self.scope.function and isinstance(actual, GenericInstanceType) and actual.generic_parent is TY_FORKED:
fty = self.scope.function.obj_type.return_type
if fty.generic_parent in PROMISES:
fty = fty.generic_args[0] # todo: check if this whole if-block works
self.scope.function.obj_type.return_type = TY_JOIN.instantiate([fty])
return actual return actual
def visit_function_call(self, ftype: ResolvedConcreteType, arguments: List[BaseType]): def visit_function_call(self, ftype: ResolvedConcreteType, arguments: List[BaseType]):
...@@ -304,9 +317,11 @@ class ScoperExprVisitor(ScoperVisitor): ...@@ -304,9 +317,11 @@ class ScoperExprVisitor(ScoperVisitor):
left = self.visit(node.value) left = self.visit(node.value)
if isinstance(left, ClassTypeType): if isinstance(left, ClassTypeType):
return self.anno().visit(node).type_type() return self.anno().visit(node).type_type()
args = node.slice if type(node.slice) == tuple else [node.slice] raise NotImplementedError("Should not happen")
args = [self.visit(e) for e in args] # desugared
return self.make_dunder([left, *args], "getitem") # args = node.slice if type(node.slice) == tuple else [node.slice]
# args = [self.visit(e) for e in args]
# return self.make_dunder([left, *args], "getitem")
def visit_UnaryOp(self, node: ast.UnaryOp) -> BaseType: def visit_UnaryOp(self, node: ast.UnaryOp) -> BaseType:
val = self.visit(node.operand) val = self.visit(node.operand)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment