commit 9e9fed8384649355805ca525330cce5138475501 Author: avitex Date: Wed Mar 13 23:10:46 2019 -0700 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6f5ced1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.vscode +__pycache__ \ No newline at end of file diff --git a/run.py b/run.py new file mode 100644 index 0000000..3db87e9 --- /dev/null +++ b/run.py @@ -0,0 +1,56 @@ +from tango.vm import TangoVM + +if __name__ == '__main__': + code = """ + ( + (def hello "world") + (def foo "bar") + (def dance ( + (def other "world") + (print (if (eq hello other) "the world is ok" "the world is broken")) + )) + (on "click" dance) + (dance) + (def hello "moon") + ) + """ + + event_handlers = [] + + def builtin_on(vm, scope, event, handler): + handler = vm.eval_in_scope(scope, handler) + event_handlers.append((event, handler, scope)) + + def trigger_event(vm, trigger_event): + results = [] + for event, handler, scope in event_handlers: + if trigger_event == event: + results.append(vm.eval_in_scope(scope, handler)) + return results + + def builtin_def(vm, scope, ident, val): + scope.set_def(ident, val) + + def builtin_print(vm, scope, *args): + print(*vm.multi_eval_in_scope(scope, args)) + + def builtin_if(vm, scope, predictate, truthy, falsy = None): + if vm.eval_in_scope(scope, predictate): + return vm.eval_in_scope(scope, truthy) + elif falsy: + return vm.eval_in_scope(scope, falsy) + else: + return None + + def builtin_eq(vm, scope, a, b): + a, b = vm.multi_eval_in_scope(scope, (a, b)) + return a == b + + vm = TangoVM() + vm.def_builtin('def', builtin_def) + vm.def_builtin('print', builtin_print) + vm.def_builtin('eq', builtin_eq) + vm.def_builtin('if', builtin_if) + vm.def_builtin('on', builtin_on) + vm.eval_string(code) + trigger_event(vm, 'click') \ No newline at end of file diff --git a/tango/__init__.py b/tango/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tango/parser.py b/tango/parser.py new file mode 100644 index 0000000..1caf970 --- /dev/null +++ b/tango/parser.py @@ -0,0 +1,92 @@ +from tango.value import TangoIdent, TangoExpr + +PAREN_OPEN = '(' +PAREN_CLOSE = ')' +DOUBLE_QUOTE = '"' +WHITESPACE = ' \t\n\r' + + +class StringReader: + def __init__(self, s): + self._src = s + self._len = len(s) + self._cur = -1 + + def next(self): + if self._len == self._cur + 1: + return None + else: + self._cur += 1 + return self._src[self._cur] + + +class TangoParser: + def __init__(self, src): + self._cur = None + self._src = src + self._next() + + def parse(self): + while self._cur is not None: + self._skip_whitespace() + if self._cur is PAREN_OPEN: + return self._parse_expr() + elif self._cur is DOUBLE_QUOTE: + return self._parse_str() + elif self._cur.isalpha(): + return self._parse_ident() + elif self._cur.isnumeric(): + return self._parse_int() + else: + self._raise_invalid_char() + + def _parse_expr(self): + expr = TangoExpr() + self._next() + while self._cur is not None: + self._skip_whitespace() + if self._cur is PAREN_CLOSE: + self._next() + return expr + else: + expr.append(self.parse()) + + self._raise_unexpected_end() + + def _parse_ident(self): + ident = self._read_until(WHITESPACE + PAREN_CLOSE) + return TangoIdent(ident) + + def _parse_str(self): + self._next() + s = self._read_until(DOUBLE_QUOTE) + self._next() + return s + + def _parse_int(self): + s = self._read_until(WHITESPACE + PAREN_CLOSE) + return int(s) + + def _read_until(self, delims): + s = '' + + while self._cur is not None and self._cur not in delims: + s += self._cur + self._next() + + return s + + def _next(self): + self._cur = self._src.next() + return self._cur + + def _raise_unexpected_end(self): + raise ValueError('Unexpected end of stream') + + def _raise_invalid_char(self): + raise ValueError('Invalid chr: ' + self._cur) + + def _skip_whitespace(self): + if self._cur in WHITESPACE: + while self._next() in WHITESPACE: + continue \ No newline at end of file diff --git a/tango/scope.py b/tango/scope.py new file mode 100644 index 0000000..bbfbd2c --- /dev/null +++ b/tango/scope.py @@ -0,0 +1,28 @@ +class TangoScope: + def __init__(self): + self.defs = {} + + def set_def(self, ident, val): + self.defs[ident] = val + + def get_def(self, ident): + return self.defs.get(ident) + +class ChildTangoScope(TangoScope): + def __init__(self, parent): + super().__init__() + self.parent = parent + + def get_def_local(self, ident): + return self.defs.get(ident) + + def get_def(self, ident): + local = self.get_def_local(ident) + if local is None: + return self.parent.get_def(ident) + else: + return local + + +class RootTangoScope(TangoScope): + pass \ No newline at end of file diff --git a/tango/utils.py b/tango/utils.py new file mode 100644 index 0000000..c169308 --- /dev/null +++ b/tango/utils.py @@ -0,0 +1,26 @@ +from tango.value import TangoIdent, TangoExpr + +def is_callable(fn): + return hasattr(fn, '__call__') + + +def is_ident(ident): + return isinstance(ident, TangoIdent) + + +def is_expr(expr): + return isinstance(expr, TangoExpr) + + +def resolve_if_ident(scope, val): + if is_ident(val): + scope.get_def(val) + else: + val + + +def resolve_idents(scope, vals): + resolved = [] + for val in vals: + resolved.append(resolve_if_ident(scope, val)) + return resolved \ No newline at end of file diff --git a/tango/value.py b/tango/value.py new file mode 100644 index 0000000..d903bdd --- /dev/null +++ b/tango/value.py @@ -0,0 +1,19 @@ +class TangoExpr(list): + + @property + def is_unit(self): + return len(self) == 0 + + @property + def is_block(self): + return len(self) > 0 and isinstance(self[0], TangoExpr) + + @property + def is_call(self): + return len(self) > 0 and isinstance(self[0], TangoIdent) + + +class TangoIdent(str): + + def __repr__(self): + return super().__str__() diff --git a/tango/vm.py b/tango/vm.py new file mode 100644 index 0000000..09b8e97 --- /dev/null +++ b/tango/vm.py @@ -0,0 +1,83 @@ +import tango.utils as utils +from tango.value import TangoExpr, TangoIdent +from tango.scope import RootTangoScope, ChildTangoScope +from tango.parser import StringReader, TangoParser + + +class TangoVM: + def __init__(self): + self.root_scope = RootTangoScope() + + def eval_string(self, s): + r = StringReader(s) + p = TangoParser(r) + e = p.parse() + return self.eval(e) + + def eval(self, expr): + if utils.is_expr(expr): + return self.eval_in_scope(self.root_scope, expr) + else: + raise ValueError('Root was not Tango Expression') + + def def_builtin(self, ident, builtin): + if isinstance(ident, str): + ident = TangoIdent(ident) + if utils.is_ident(ident): + self.root_scope.set_def(ident, builtin) + else: + raise ValueError('Not ident') + + def eval_in_scope(self, scope, val): + if utils.is_expr(val): + if val.is_unit: + return None + elif val.is_block: + return self._do_block(scope, val) + elif val.is_call: + return self._do_call(scope, val) + if utils.is_ident(val): + return scope.get_def(val) + + return val + + def multi_eval_in_scope(self, scope, vals): + return [self.eval_in_scope(scope, val) for val in vals] + + def _do_eval_child(self, scope, expr, args = TangoExpr()): + child_scope = ChildTangoScope(scope) + child_scope.set_def('$', args) + return self.eval_in_scope(child_scope, expr) + + + def _do_block(self, scope, block_expr): + for expr in block_expr: + res = self.eval_in_scope(scope, expr) + return res + + def _do_call(self, scope, call_expr): + fn_ident, *fn_args = call_expr + fn_args = TangoExpr(fn_args) + fn_body = scope.get_def(fn_ident) + + if fn_body is None: + TangoVM._rt_error('ident `{}` is undefined'.format(fn_ident)) + + if utils.is_callable(fn_body): + return fn_body(self, scope, *fn_args) + + if utils.is_expr(fn_body): + if fn_body.is_unit: + return None + if fn_body.is_block: + return self._do_eval_child(scope, fn_body, fn_args) + elif fn_body.is_call: + fn_body = self._do_call(scope, fn_body) + return self._do_eval_child(scope, fn_body, fn_args) + + TangoVM._rt_error('fatal error') + + @staticmethod + def _rt_error(msg): + raise RuntimeError(msg) +