Hello, world!
This commit is contained in:
parent
5531b80331
commit
02c5f61bb8
7 changed files with 213 additions and 6 deletions
|
@ -7,3 +7,15 @@ py_project(
|
||||||
py_requirement("hypothesis"),
|
py_requirement("hypothesis"),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
py_binary(
|
||||||
|
name = "repl",
|
||||||
|
main = "src/python/lilith/repl.py",
|
||||||
|
imports = [
|
||||||
|
"src/python",
|
||||||
|
],
|
||||||
|
deps = [
|
||||||
|
":lilith",
|
||||||
|
py_requirement("prompt_toolkit"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
96
projects/lilith/src/python/lilith/interpreter.py
Normal file
96
projects/lilith/src/python/lilith/interpreter.py
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import typing as t
|
||||||
|
|
||||||
|
from .parser import Apply, Block
|
||||||
|
from .reader import Module
|
||||||
|
|
||||||
|
|
||||||
|
class Runtime(t.NamedTuple):
|
||||||
|
name: str
|
||||||
|
modules: t.Dict[str, Module]
|
||||||
|
|
||||||
|
|
||||||
|
class BindingNotFound(KeyError):
|
||||||
|
def __init__(self, msg, context):
|
||||||
|
super().__init__(msg)
|
||||||
|
self.context = context
|
||||||
|
|
||||||
|
|
||||||
|
class Bindings(object):
|
||||||
|
def __init__(self, name=None, parent=None):
|
||||||
|
self._name = None
|
||||||
|
self._parent = None
|
||||||
|
self._bindings = {}
|
||||||
|
|
||||||
|
def get(self, key):
|
||||||
|
if key in self._bindings:
|
||||||
|
return self._bindings.get(key)
|
||||||
|
elif self._parent:
|
||||||
|
try:
|
||||||
|
return self._parent.get(key)
|
||||||
|
except BindingNotFound as e:
|
||||||
|
raise BindingNotFound(str(e), self)
|
||||||
|
else:
|
||||||
|
raise BindingNotFound(f"No binding key {key}", self)
|
||||||
|
|
||||||
|
|
||||||
|
def lookup(runtime, mod, locals, name):
|
||||||
|
"""Implement resolving a name against multiple namespaces."""
|
||||||
|
|
||||||
|
err = None
|
||||||
|
try:
|
||||||
|
return locals.get(name)
|
||||||
|
except BindingNotFound as e:
|
||||||
|
err = e
|
||||||
|
|
||||||
|
if name in mod.defs:
|
||||||
|
return mod.defs.get(name)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise err
|
||||||
|
|
||||||
|
# FIXME (arrdem 2021-08-21):
|
||||||
|
# How do we ever get references to stuff in other modules?
|
||||||
|
# !import / !require is probably The Way
|
||||||
|
|
||||||
|
|
||||||
|
def eval(ctx: Runtime, mod: Module, locals: Bindings, expr):
|
||||||
|
"""Eval.
|
||||||
|
|
||||||
|
In the context of a given runtime and module which must exist within the
|
||||||
|
given runtime, evaluate the given expression recursively.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Pointedly not assert that the module is ACTUALLY in the runtime,
|
||||||
|
# We're just going to assume this for convenience.
|
||||||
|
|
||||||
|
if isinstance(expr, Apply):
|
||||||
|
# FIXME (arrdem 2021-08-21):
|
||||||
|
# Apply should be (apply <expr> <args> <kwargs>).
|
||||||
|
# Now no distinction is made between strings ("") and symbols/barewords
|
||||||
|
fun = lookup(ctx, mod, locals, expr.name)
|
||||||
|
# Evaluate the parameters
|
||||||
|
args = eval(ctx, mod, locals, expr.args.positionals)
|
||||||
|
kwargs = eval(ctx, mod, locals, expr.args.kwargs)
|
||||||
|
# Use Python's __call__ protocol
|
||||||
|
return fun(*args, **kwargs)
|
||||||
|
|
||||||
|
elif isinstance(expr, (int, float, str)):
|
||||||
|
return expr
|
||||||
|
|
||||||
|
elif isinstance(expr, list):
|
||||||
|
return [eval(ctx, mod, locals, i) for i in expr]
|
||||||
|
|
||||||
|
elif isinstance(expr, tuple):
|
||||||
|
return tuple(eval(ctx, mod, locals, i) for i in expr)
|
||||||
|
|
||||||
|
elif isinstance(expr, dict):
|
||||||
|
return {eval(ctx, mod, locals, k): eval(ctx, mod, locals, v)
|
||||||
|
for k, v in expr.items()}
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"Can't eval {expr}")
|
|
@ -22,7 +22,7 @@ class Args(t.NamedTuple):
|
||||||
|
|
||||||
|
|
||||||
class Apply(t.NamedTuple):
|
class Apply(t.NamedTuple):
|
||||||
tag: str
|
name: str
|
||||||
args: Args
|
args: Args
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,6 +44,11 @@ class Block(t.NamedTuple):
|
||||||
|
|
||||||
|
|
||||||
class TreeToTuples(lark.Transformer):
|
class TreeToTuples(lark.Transformer):
|
||||||
|
def string(self, args):
|
||||||
|
# FIXME (arrdem 2021-08-21):
|
||||||
|
# Gonna have to do escape sequences here
|
||||||
|
return args[0].value
|
||||||
|
|
||||||
def int(self, args):
|
def int(self, args):
|
||||||
return int(args[0])
|
return int(args[0])
|
||||||
|
|
||||||
|
@ -66,7 +71,6 @@ class TreeToTuples(lark.Transformer):
|
||||||
def application(self, args):
|
def application(self, args):
|
||||||
tag = args[0]
|
tag = args[0]
|
||||||
args = args[1] if len(args) > 1 else Args()
|
args = args[1] if len(args) > 1 else Args()
|
||||||
print(args)
|
|
||||||
return Apply(tag, args)
|
return Apply(tag, args)
|
||||||
|
|
||||||
def args(self, args):
|
def args(self, args):
|
||||||
|
@ -107,6 +111,10 @@ def parser_with_transformer(grammar, start="header"):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def parse_expr(buff: str):
|
||||||
|
return parser_with_transformer(GRAMMAR, "expr").parse(buff)
|
||||||
|
|
||||||
|
|
||||||
def parse_buffer(buff: str, name: str = "&buff") -> t.List[object]:
|
def parse_buffer(buff: str, name: str = "&buff") -> t.List[object]:
|
||||||
header_parser = parser_with_transformer(GRAMMAR, "header")
|
header_parser = parser_with_transformer(GRAMMAR, "header")
|
||||||
|
|
||||||
|
|
|
@ -19,9 +19,7 @@ def read_buffer(buffer: str, name: str = "&buff") -> Module:
|
||||||
|
|
||||||
m = Module(name, {})
|
m = Module(name, {})
|
||||||
for block in parse_buffer(buffer, name):
|
for block in parse_buffer(buffer, name):
|
||||||
log.debug(f"{name}, Got a block", block)
|
if block.app.name == "def":
|
||||||
|
|
||||||
if block.tag == "def":
|
|
||||||
if len(block.args.positionals) == 2:
|
if len(block.args.positionals) == 2:
|
||||||
def_name, expr = block.args.positionals
|
def_name, expr = block.args.positionals
|
||||||
m.defs[def_name] = Block(expr, block.body_lines)
|
m.defs[def_name] = Block(expr, block.body_lines)
|
||||||
|
|
60
projects/lilith/src/python/lilith/repl.py
Normal file
60
projects/lilith/src/python/lilith/repl.py
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
"""A simple Lilith shell."""
|
||||||
|
|
||||||
|
from lilith.interpreter import Bindings, eval, Runtime
|
||||||
|
from lilith.parser import Apply, Args, parse_expr
|
||||||
|
from lilith.reader import Module
|
||||||
|
|
||||||
|
from prompt_toolkit import print_formatted_text, prompt, PromptSession
|
||||||
|
from prompt_toolkit.formatted_text import FormattedText
|
||||||
|
from prompt_toolkit.history import FileHistory
|
||||||
|
from prompt_toolkit.styles import Style
|
||||||
|
|
||||||
|
|
||||||
|
STYLE = Style.from_dict(
|
||||||
|
{
|
||||||
|
# User input (default text).
|
||||||
|
"": "",
|
||||||
|
"prompt": "ansigreen",
|
||||||
|
"time": "ansiyellow",
|
||||||
|
"result": "ansiblue",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def print_(fmt, **kwargs):
|
||||||
|
print_formatted_text(FormattedText(fmt), **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
session = PromptSession(history=FileHistory(".lilith.history"))
|
||||||
|
runtime = Runtime("test", dict())
|
||||||
|
module = Module("__repl__", {
|
||||||
|
"print": print,
|
||||||
|
"int": int,
|
||||||
|
"string": str,
|
||||||
|
"float": float,
|
||||||
|
})
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
line = session.prompt([("class:prompt", ">>> ")], style=STYLE)
|
||||||
|
except (KeyboardInterrupt):
|
||||||
|
continue
|
||||||
|
except EOFError:
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
expr = parse_expr(line)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = eval(
|
||||||
|
runtime, module,
|
||||||
|
Bindings("__root__", None),
|
||||||
|
expr
|
||||||
|
)
|
||||||
|
print_([("class:result", f"⇒ {result!r}")], style=STYLE)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
continue
|
34
projects/lilith/test/python/test_interpreter.py
Normal file
34
projects/lilith/test/python/test_interpreter.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from lilith.interpreter import Bindings, Runtime, eval
|
||||||
|
from lilith.reader import Module
|
||||||
|
from lilith.parser import Args, Apply
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('expr, expected', [
|
||||||
|
(1, 1),
|
||||||
|
([1, 2], [1, 2]),
|
||||||
|
({"foo": "bar"}, {"foo": "bar"}),
|
||||||
|
])
|
||||||
|
def test_eval(expr, expected):
|
||||||
|
assert eval(
|
||||||
|
Runtime("test", dict()),
|
||||||
|
Module("__repl__", dict()),
|
||||||
|
Bindings("__root__", None),
|
||||||
|
expr
|
||||||
|
) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_hello_world(capsys):
|
||||||
|
assert eval(
|
||||||
|
Runtime("test", {}),
|
||||||
|
Module("__repl__", {"print": print}),
|
||||||
|
Bindings("__root__", None),
|
||||||
|
Apply("print", Args(["hello, world"], {}))
|
||||||
|
) is None
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert captured.out == "hello, world\n"
|
|
@ -12,5 +12,4 @@ import pytest
|
||||||
])
|
])
|
||||||
def test_read(example, expected):
|
def test_read(example, expected):
|
||||||
got = read_buffer(example)
|
got = read_buffer(example)
|
||||||
print(got)
|
|
||||||
assert got == expected
|
assert got == expected
|
||||||
|
|
Loading…
Reference in a new issue