Skip to content

Commit

Permalink
added vkscript converter
Browse files Browse the repository at this point in the history
  • Loading branch information
kesha1225 committed Apr 6, 2020
1 parent 23e7892 commit b63cf87
Show file tree
Hide file tree
Showing 9 changed files with 437 additions and 0 deletions.
12 changes: 12 additions & 0 deletions vkwave/bots/vkscript/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import vkwave.bots.vkscript.handlers.assignments
import vkwave.bots.vkscript.handlers.blocks
import vkwave.bots.vkscript.handlers.calls
import vkwave.bots.vkscript.handlers.expressions
import vkwave.bots.vkscript.handlers.statements
import vkwave.bots.vkscript.handlers.types
from .converter import VKScriptConverter
from .execute import Execute
from .execute import execute


__all__ = ("execute", "Execute", "VKScriptConverter")
58 changes: 58 additions & 0 deletions vkwave/bots/vkscript/converter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import typing
import contextvars

import pydantic

from typing import Type
from typing import TypeVar


T = TypeVar("T")


class ContextInstanceMixin:
def __init_subclass__(cls, **kwargs):
cls.__context_instance = contextvars.ContextVar("instance_" + cls.__name__)
return cls

@classmethod
def get_current(cls: Type[T], no_error=True) -> T:
if no_error:
return cls.__context_instance.get(None)
return cls.__context_instance.get()

@classmethod
def set_current(cls: Type[T], value: T):
if not isinstance(value, cls):
raise TypeError(
f"Value should be instance of '{cls.__name__}' not '{type(value).__name__}'"
)
cls.__context_instance.set(value)


class Scope(pydantic.BaseModel):
locals: list = []
globals: dict = {}


class VKScriptConverter(ContextInstanceMixin):
handlers: dict = {}

@classmethod
def register(cls, expr):
def meta(handler: typing.Callable):
cls.handlers[expr] = handler

return meta

def __init__(self, scope: Scope = None):
self.scope = scope or Scope()
self.set_current(self)

def convert_node(self, node):
if node.__class__ in self.handlers:
return self.handlers[node.__class__](node)
raise NotImplementedError(f"Conversion for type {node.__class__} not implemented.")

def convert_block(self, nodes: list):
return "".join(self.convert_node(child) for child in nodes)
55 changes: 55 additions & 0 deletions vkwave/bots/vkscript/execute.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import ast
import inspect
import types

from vkwave.bots.vkscript.converter import Scope
from vkwave.bots.vkscript.converter import VKScriptConverter


def execute(func: types.FunctionType):
e = Execute()
return e.decorate(func)


class Execute:
_code = None
_preprocessor = None
_func = None

def decorate(self, func):
source = inspect.getsource(func)
self._func = func
self._code = ast.parse(source).body[0]
return self

def preprocessor(self, func):
self._preprocessor = func

def build(self, *args, **kwargs) -> str:
if self._code.__class__ == ast.FunctionDef:
globals_ = dict(self._func.__globals__)
for i, argument in enumerate(self._code.args.args):
if argument.arg in kwargs:
globals_[argument.arg] = kwargs[argument.arg]
elif i < len(args):
globals_[argument.arg] = args[i]
elif argument.arg.upper() == "API":
continue
else:
raise TypeError(f"missing required argument {argument.arg}")
converter = VKScriptConverter(Scope(globals=globals_))
return converter.convert_block(self._code.body)
raise NotImplementedError()

async def __call__(self, *args, **kwargs):
if self._preprocessor is not None:
return await self._preprocessor(*args, **kwargs)
return await self.execute(*args, **kwargs)

async def execute(self, *args, **kwargs):
vk = _get_vk().get_current()
code = self.build(*args, **kwargs)
response = await vk.api_request("execute", {"code": code})
return response

e = execute
56 changes: 56 additions & 0 deletions vkwave/bots/vkscript/handlers/assignments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import ast

from ..converter import VKScriptConverter
from .expressions import OPS


@VKScriptConverter.register(ast.Assign)
def assign_handler(node: ast.Assign):
converter = VKScriptConverter.get_current()
left = node.targets
left_ = []
for target in left:
if target.__class__ == ast.Name:
left_.append(target.id)
converter.scope.locals.append(target.id)
elif target.__class__ == ast.Subscript:
pass
elif target.__class__ == ast.Tuple:
raise NotImplementedError("Tuple assignments are not allowed")
else:
raise NotImplementedError(f"Assignments of {target.__class__} are not implemented")

right = converter.convert_node(node.value)
return "var " + ",".join(f"{target}={right}" for target in left_) + ";"


@VKScriptConverter.register(ast.AugAssign)
def aug_assign_handler(node: ast.AugAssign):
converter = VKScriptConverter.get_current()

if node.op.__class__ not in OPS:
raise NotImplementedError(f"Operation {node.op} is not implemented.")

if node.target.__class__ == ast.Name and node.target.id not in converter.scope.locals:
raise NameError(f"name '{node.target.id}' is not defined")
target = converter.convert_node(node.target)
return f"{target}={target}{OPS[node.op.__class__]}({converter.convert_node(node.value)});"


@VKScriptConverter.register(ast.Name)
def name_handler(node: ast.Name):
converter = VKScriptConverter.get_current()
if node.id in converter.scope.locals:
return node.id
if node.id not in converter.scope.globals:
raise NameError(f"name '{node.id}' is not defined")
if (
type(converter.scope.globals[node.id]) not in (str, int, tuple, dict, list)
and converter.scope.globals[node.id] is not None
):
raise NotImplementedError(
f'type "{type(converter.scope.globals[node.id])}" not allowed inside VK Script'
)
return converter.convert_node(
ast.parse(repr(converter.scope.globals[node.id]), mode="eval").body
)
28 changes: 28 additions & 0 deletions vkwave/bots/vkscript/handlers/blocks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import ast

from ..converter import VKScriptConverter

WHILE_TEMPLATE = "while(%(test)s){%(body)s};"
IF_TEMPLATE = "if(%(test)s){%(content)s}%(other)s;"


@VKScriptConverter.register(ast.While)
def while_handler(node: ast.While):
converter = VKScriptConverter.get_current()
if node.orelse:
raise NotImplementedError("while...else not implemented.")
test = converter.convert_node(node.test)
body = converter.convert_block(node.body)
return WHILE_TEMPLATE % {"test": test, "body": body}


@VKScriptConverter.register(ast.If)
def if_handler(node: ast.If):
converter = VKScriptConverter.get_current()
test = converter.convert_node(node.test)
content = converter.convert_block(node.body)
if node.orelse:
other = f"else{{{converter.convert_block(node.orelse)}}}"
else:
other = ""
return IF_TEMPLATE % {"test": test, "content": content, "other": other}
58 changes: 58 additions & 0 deletions vkwave/bots/vkscript/handlers/calls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import ast

from ..converter import VKScriptConverter


@VKScriptConverter.register(ast.Call)
def call_handler(node: ast.Call):
converter = VKScriptConverter.get_current()
replacements = {"append": "push", "pop": "pop", "split": "split"}
funcs = [
"slice",
"push",
"pop",
"shift",
"unshift",
"splice",
"substr",
"split",
"append",
]
attrs = []
node_ = node.func
while isinstance(node_, ast.Attribute):
attrs.append(node_.attr)
node_ = node_.value

if node_.id.upper() == "API" and len(attrs) >= 1:
if node.args:
raise TypeError("api calls does not accept positional arguments")

inner = ",".join(
f"{keyword.arg}:{converter.convert_node(keyword.value)}" for keyword in node.keywords
)

return "API." + ".".join(attrs[::-1]) + f"({{{inner}}})"

elif not attrs and node_.id == "len" and "len" not in converter.scope.globals:
return f"{converter.convert_node(node.args[0])}.length"

# TODO: rewrite?
elif len(attrs) == 1 and attrs[0] in funcs:
if node.keywords:
raise NotImplementedError("keywords not allowed")

if attrs[0] in replacements:
func = replacements[attrs[0]]
else:
func = attrs[0]

return (
f"{converter.convert_node(node_)}.{func}"
f'({",".join(converter.convert_node(arg) for arg in node.args)})'
)

else:
raise NotImplementedError(
f'function call "{node_.id + ("." if attrs else "") + ".".join(attrs[::-1])}" not allowed inside VK Script!'
)
105 changes: 105 additions & 0 deletions vkwave/bots/vkscript/handlers/expressions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import ast
import string

from ..converter import VKScriptConverter


OPS = {
ast.Add: "+",
ast.Sub: "-",
ast.Mult: "*",
ast.Div: "/",
ast.Pow: "**",
ast.RShift: ">>",
ast.LShift: "<<",
ast.BitOr: "|",
ast.BitAnd: "&",
ast.Mod: "%",
}


@VKScriptConverter.register(ast.Expr)
def expr_handler(node: ast.Expr):
converter = VKScriptConverter.get_current()
return converter.convert_node(node.value) + ";"


@VKScriptConverter.register(ast.Module)
def module_handler(node: ast.Module):
converter = VKScriptConverter.get_current()
return converter.convert_block(node.body)


@VKScriptConverter.register(ast.BinOp)
def bin_op_handler(node: ast.BinOp):
if node.op.__class__ not in OPS:
raise NotImplementedError(f"Operation {node.op} is not implemented.")
converter = VKScriptConverter.get_current()
return f"{converter.convert_node(node.left)}{OPS[node.op.__class__]}{converter.convert_node(node.right)}"


@VKScriptConverter.register(ast.Compare)
def compare_handler(node: ast.Compare):
ops = {
ast.Gt: ">",
ast.Lt: "<",
ast.GtE: ">=",
ast.LtE: "<=",
ast.Eq: "==",
ast.NotEq: "!=",
}

operations = []
converter = VKScriptConverter.get_current()
left = converter.convert_node(node.left)
for op, comparator in zip(node.ops, node.comparators):
if op.__class__ not in ops:
raise NotImplementedError(f"comparison operator {op} not supported")
operations.append(f"{left}{ops[op.__class__]}{converter.convert_node(comparator)}")
return "&&".join(operations)


@VKScriptConverter.register(ast.BoolOp)
def bool_op_handler(node: ast.BoolOp):
ops = {ast.And: "&&", ast.Or: "||"}
if node.op.__class__ not in ops:
raise NotImplementedError(f"operation '{node.op}' not supported")
converter = VKScriptConverter.get_current()
return ops[node.op.__class__].join(converter.convert_node(value) for value in node.values)


@VKScriptConverter.register(ast.UnaryOp)
def unary_op_handler(node: ast.UnaryOp):
ops = {ast.UAdd: "+", ast.USub: "-"}
if node.op.__class__ not in ops:
raise NotImplementedError(f"operation '{node.op}' not supported")
converter = VKScriptConverter.get_current()
return f"{ops[node.op.__class__]}{converter.convert_node(node.operand)}"


@VKScriptConverter.register(ast.Subscript)
def subscript_handler(node: ast.Subscript):
converter = VKScriptConverter.get_current()
value = converter.convert_node(node.value)
if node.slice.__class__ == ast.Index:
safe = frozenset(string.ascii_letters + string.digits + "_")
if node.slice.value.__class__ == ast.Str and set(node.slice.value.s) <= safe:
# TODO: Improve safety check, first symbol may be digit
return f"{value}.{node.slice.value.s}"
return f"{value}[{converter.convert_node(node.slice.value)}]"
elif node.slice.__class__ == ast.Slice:
if node.slice.step:
raise NotImplementedError("steps in slice not supported")
if node.slice.lower.__class__ != ast.Num and node.slice.upper.__class__ != ast.Num:
raise TypeError("slices must be integers")
lower = node.slice.lower.n or 0
if node.slice.upper:
return f"{value}.slice({lower},{node.slice.upper.n})"
return f"{value}.slice({lower})"
raise NotImplementedError(f"slice {node.slice} not supported")


@VKScriptConverter.register(ast.Attribute)
def attribute_handler(node: ast.Attribute):
converter = VKScriptConverter.get_current()
return f"{converter.convert_node(node.value)}.{node.attr}"
Loading

0 comments on commit b63cf87

Please sign in to comment.