Hello, world!

This commit is contained in:
Reid 'arrdem' McKenzie 2021-08-21 14:07:57 -06:00
parent 5531b80331
commit 02c5f61bb8
7 changed files with 213 additions and 6 deletions

View file

@ -7,3 +7,15 @@ py_project(
py_requirement("hypothesis"),
]
)
py_binary(
name = "repl",
main = "src/python/lilith/repl.py",
imports = [
"src/python",
],
deps = [
":lilith",
py_requirement("prompt_toolkit"),
]
)

View 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}")

View file

@ -22,7 +22,7 @@ class Args(t.NamedTuple):
class Apply(t.NamedTuple):
tag: str
name: str
args: Args
@ -44,6 +44,11 @@ class Block(t.NamedTuple):
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):
return int(args[0])
@ -66,7 +71,6 @@ class TreeToTuples(lark.Transformer):
def application(self, args):
tag = args[0]
args = args[1] if len(args) > 1 else Args()
print(args)
return Apply(tag, 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]:
header_parser = parser_with_transformer(GRAMMAR, "header")

View file

@ -19,9 +19,7 @@ def read_buffer(buffer: str, name: str = "&buff") -> Module:
m = Module(name, {})
for block in parse_buffer(buffer, name):
log.debug(f"{name}, Got a block", block)
if block.tag == "def":
if block.app.name == "def":
if len(block.args.positionals) == 2:
def_name, expr = block.args.positionals
m.defs[def_name] = Block(expr, block.body_lines)

View 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

View 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"

View file

@ -12,5 +12,4 @@ import pytest
])
def test_read(example, expected):
got = read_buffer(example)
print(got)
assert got == expected