Hello, world!
This commit is contained in:
parent
1b97cfb41d
commit
ed41277614
7 changed files with 213 additions and 6 deletions
|
@ -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"),
|
||||
]
|
||||
)
|
||||
|
|
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):
|
||||
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")
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
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):
|
||||
got = read_buffer(example)
|
||||
print(got)
|
||||
assert got == expected
|
||||
|
|
Loading…
Reference in a new issue