diff --git a/projects/lilith/BUILD b/projects/lilith/BUILD index 0935c95..05c23d1 100644 --- a/projects/lilith/BUILD +++ b/projects/lilith/BUILD @@ -2,6 +2,8 @@ py_project( name = "lib", lib_deps = [ py_requirement("lark"), + py_requirement("pyyaml"), + py_requirement("markdown"), ], test_deps = [ py_requirement("hypothesis"), @@ -13,7 +15,10 @@ zapp_binary( main = "src/python/lilith/__main__.py", deps = [ ":lib", - py_requirement("lark"), # FIXME: Absolutely a zapp bug + # FIXME: Due to zapp bug(s), replicating requirements here. + py_requirement("lark"), + py_requirement("pyyaml"), + py_requirement("markdown"), py_requirement("prompt_toolkit"), ], ) diff --git a/projects/lilith/src/lilith/designdoc.lil b/projects/lilith/src/lilith/designdoc.lil index 917d0b5..5b61545 100644 --- a/projects/lilith/src/lilith/designdoc.lil +++ b/projects/lilith/src/lilith/designdoc.lil @@ -1,4 +1,4 @@ -!def[pitch, frag[lang: md]] +!def[pitch, md[]] # The Lilith Pitch Code is more than .. just code for the compiler. @@ -14,8 +14,7 @@ idea: the parser is going to IGNORE stuff by default. Let's build an M-expression syntax with support for multiple co-equal languages and fragments/scopes with a meta-syntax allowing for references between them and consuming fragment-notation-defined blocks? (FIXME: better word?) -!def[sytnax] -!frag[lang: md] +!def[sytnax, md[]] \![] bang-brackets is a meta-syntactic directive used both by the lil tangler and the lil object language. @@ -35,10 +34,9 @@ FIXME: we need bare words, we need strings FIXME: We need the object language -!def[openapi, frag[lang: yaml]] +!def[openapi, yaml[]] -!def[main, frag[lang: lil]] +!def[main, lilith[]] ; is importing a bang-operation? -import[tagle] -print[str.join["", list[pitch, syntax]]] +print[str.join["", [pitch, syntax]]] diff --git a/projects/lilith/src/python/lilith/__main__.py b/projects/lilith/src/python/lilith/__main__.py index 8680146..2151a12 100644 --- a/projects/lilith/src/python/lilith/__main__.py +++ b/projects/lilith/src/python/lilith/__main__.py @@ -1,15 +1,18 @@ -"""A simple Lilith shell.""" +"""The Lilith runner.""" +import argparse +from importlib.resources import read_text as resource_text +import sys import traceback from lilith.interpreter import Bindings, eval, Runtime -from lilith.parser import Apply, Args, parse_expr -from lilith.reader import Module - +from lilith.parser import Apply, Args, parse_expr, Symbol +from lilith.reader import Import, Module, read_buffer, read_file 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 +import yaml STYLE = Style.from_dict( @@ -27,17 +30,13 @@ def print_(fmt, **kwargs): print_formatted_text(FormattedText(fmt), **kwargs) -if __name__ == "__main__": +def repl(opts, args, runtime): + """Run an interactive Lilith repl, supporting only the Lilith language itself.""" + session = PromptSession(history=FileHistory(".lilith.history")) - runtime = Runtime("test", dict()) module = Module( "__repl__", - { - "print": print, - "int": int, - "string": str, - "float": float, - }, + {}, ) while True: @@ -60,3 +59,88 @@ if __name__ == "__main__": except Exception as e: traceback.print_exc() continue + + +def batch(opts, args, runtime): + """Run a Lilith program 'for real' in non-interactive batch mode.""" + + if opts.file: + mod = read_file(opts.file) + main = "main" + elif opts.main: + mod, main = opts.main.split(":") if ":" in opts.main else opts.main, "main" + dirmod, rname = ".".join(mod.split(".")[:-1]), mod.split(".")[-1] + ".lil" + mod = read_buffer(resource_text(dirmod, rname), mod) + + main = Symbol(main) + + # Register + runtime.modules[mod.name] = mod + + print("DEBUG: batch mode") + print( + yaml.dump( + { + "type": "runtime", + "name": runtime.name, + "modules": { + m.name: { + "defs": {dname.name: repr(d) for dname, d in m.defs.items()} + } + for m in runtime.modules.values() + }, + } + ) + ) + + if main in mod.defs: + eval(runtime, mod, Bindings(main, None), mod.defs.get(main)) + else: + raise NameError(f"entry point {main} not found in {mod.name.name}") + + +parser = argparse.ArgumentParser() +parser.add_argument( + "-m", "--main", default="main", help="The name of an entry point eg :" +) +parser.add_argument( + "-p", + "--path", + default=[], + action="append", + help="Append something to the module path.", +) +parser.add_argument( + "--prelude", default="lilith.prelude", help="Select a module prelude." +) +parser.add_argument("file", nargs="?", help="A file to start executing from") + + +if __name__ == "__main__": + opts, args = parser.parse_known_args() + + # Bash anything the user says is the path onto the PYTHONPATH so we can use importlib for our loading machinery. + for e in opts.path: + for _e in e.split(":"): + sys.path.insert(0, _e) + + # Building up a bootstrap interface for going out to many Python features. + runtime = Runtime("__runtime__", Symbol(opts.prelude), {}) + bootstrap = Module( + "lilith.bootstrap", + [], + {Symbol("py"): lambda *args, body=None, **kwargs: eval(body)}, + ) + runtime.modules[Symbol(bootstrap.name)] = bootstrap + prelude_mod = read_buffer( + resource_text( + ".".join(opts.prelude.split(".")[:-1]), opts.prelude.split(".")[-1] + ".lil" + ), + opts.prelude, + ) + runtime.modules[prelude_mod.name] = prelude_mod + + if (opts.path and opts.main) or opts.file: + batch(opts, args, runtime) + else: + repl(opts, args, runtime) diff --git a/projects/lilith/src/python/lilith/interpreter.py b/projects/lilith/src/python/lilith/interpreter.py index a4b0c9d..a379b0d 100644 --- a/projects/lilith/src/python/lilith/interpreter.py +++ b/projects/lilith/src/python/lilith/interpreter.py @@ -1,16 +1,17 @@ """ - +A quick and dirty recursive interpreter for Lilith. """ import typing as t -from .parser import Apply, Block, Symbol -from .reader import Module +from lilith.parser import Apply, Block, Symbol +from lilith.reader import Module class Runtime(t.NamedTuple): name: str - modules: t.Dict[str, Module] + prelude: Symbol + modules: t.Dict[Symbol, Module] class BindingNotFound(KeyError): diff --git a/projects/lilith/src/python/lilith/prelude.lil b/projects/lilith/src/python/lilith/prelude.lil new file mode 100644 index 0000000..a859a5e --- /dev/null +++ b/projects/lilith/src/python/lilith/prelude.lil @@ -0,0 +1,129 @@ +!import[lilith.bootstrap, [lang, py]] +!def[abs, lang[py]] +abs +!def[delattr, lang[py]] +delattr +!def[hash, lang[py]] +hash +!def[set, lang[py]] +set +!def[all, lang[py]] +all +!def[dict, lang[py]] +dict +!def[min, lang[py]] +min +!def[any, lang[py]] +any +!def[dir, lang[py]] +dir +!def[hex, lang[py]] +hex +!def[next, lang[py]] +next +!def[slice, lang[py]] +slice +!def[ascii, lang[py]] +ascii +!def[divmod, lang[py]] +divmod +!def[id, lang[py]] +id +!def[object, lang[py]] +object +!def[sorted, lang[py]] +sorted +!def[bin, lang[py]] +bin +!def[enumerate, lang[py]] +enumerate +!def[input, lang[py]] +input +!def[oct, lang[py]] +oct +!def[staticmethod, lang[py]] +staticmethod +!def[bool, lang[py]] +bool +!def[eval, lang[py]] +eval +!def[int, lang[py]] +int +!def[open, lang[py]] +open +!def[str, lang[py]] +str +!def[breakpoint, lang[py]] +breakpoint +!def[exec, lang[py]] +exec +!def[isinstance, lang[py]] +isinstance +!def[ord, lang[py]] +ord +!def[sum, lang[py]] +sum +!def[bytearray, lang[py]] +bytearray +!def[filter, lang[py]] +filter +!def[issubclass, lang[py]] +issubclass +!def[pow, lang[py]] +pow +!def[super, lang[py]] +super +!def[bytes, lang[py]] +bytes +!def[float, lang[py]] +float +!def[iter, lang[py]] +iter +!def[print, lang[py]] +print +!def[tuple, lang[py]] +tuple +!def[callable, lang[py]] +callable +!def[format, lang[py]] +format +!def[len, lang[py]] +len +!def[property, lang[py]] +property +!def[type, lang[py]] +type +!def[chr, lang[py]] +chr +!def[frozenset, lang[py]] +frozenset +!def[list, lang[py]] +list +!def[range, lang[py]] +range +!def[vars, lang[py]] +vars +!def[classmethod, lang[py]] +classmethod +!def[getattr, lang[py]] +getattr +!def[locals, lang[py]] +locals +!def[repr, lang[py]] +repr +!def[zip, lang[py]] +zip +!def[compile, lang[py]] +compile +!def[map, lang[py]] +map +!def[reversed, lang[py]] +reversed +!def[complex, lang[py]] +complex +!def[hasattr, lang[py]] +hasattr +!def[max, lang[py]] +max +!def[round, lang[py]] +round diff --git a/projects/lilith/src/python/lilith/reader.py b/projects/lilith/src/python/lilith/reader.py index b676837..997d94c 100644 --- a/projects/lilith/src/python/lilith/reader.py +++ b/projects/lilith/src/python/lilith/reader.py @@ -4,21 +4,30 @@ Lilith's reader takes parsed blocks and applies languages, building a module str import logging import typing as t -from .parser import Block, Args, parse_buffer, Symbol from warnings import warn +from lilith.parser import Args, Block, parse_buffer, Symbol + + log = logging.getLogger(__name__) +class Import(t.NamedTuple): + src: Symbol + names: t.Dict[Symbol, Symbol] + wild: bool = False + + class Module(t.NamedTuple): - name: str + name: Symbol + imports: t.List[Import] defs: t.Dict[str, Block] def read_buffer(buffer: str, name: str = "&buff") -> Module: """Read a module out of a string [or file]""" - m = Module(name, {}) + m = Module(Symbol(name), [], {}) for block in parse_buffer(buffer, name): if block.app.target == Symbol("def"): if len(block.args.positionals) == 2: @@ -30,6 +39,20 @@ def read_buffer(buffer: str, name: str = "&buff") -> Module: if block.args.kwargs: warn("!def[] are ignored") + elif block.app.target == Symbol("import"): + # FIXME (arrdem 2021-08-21): + # This doesn't simplify imports as it goes. + # Multiple imports from the same source will wind up with multiple importlist entries. + iname = block.args.positionals[0] + wild = block.args.kwargs.get(Symbol("wild"), False) + rename = block.args.kwargs.get(Symbol("as"), {}) + imports = ( + block.args.positionals[1] if len(block.args.positionals) == 2 else [] + ) + m.imports.append( + Import(iname, {rename.get(i, i): i for i in imports}, wild) + ) + else: raise SyntaxError(f"Unsupported block !{block.tag}[..]") diff --git a/projects/lilith/test/python/conftest.py b/projects/lilith/test/python/conftest.py index 6723c47..25609e7 100644 --- a/projects/lilith/test/python/conftest.py +++ b/projects/lilith/test/python/conftest.py @@ -2,8 +2,7 @@ Pytest fixtures. """ -from lilith.parser import Block, parser_with_transformer, GRAMMAR - +from lilith.parser import Block, GRAMMAR, parser_with_transformer import pytest diff --git a/projects/lilith/test/python/test_interpreter.py b/projects/lilith/test/python/test_interpreter.py index 3351c18..2a39061 100644 --- a/projects/lilith/test/python/test_interpreter.py +++ b/projects/lilith/test/python/test_interpreter.py @@ -2,13 +2,17 @@ """ -from lilith.interpreter import Bindings, Runtime, eval +from lilith.interpreter import Bindings, eval, Runtime +from lilith.parser import Apply, Args, Symbol from lilith.reader import Module -from lilith.parser import Args, Apply, Symbol - import pytest +@pytest.fixture +def runtime(): + return Runtime("test", None, {}) + + @pytest.mark.parametrize( "expr, expected", [ @@ -17,11 +21,11 @@ import pytest ({"foo": "bar"}, {"foo": "bar"}), ], ) -def test_eval(expr, expected): +def test_eval(runtime, expr, expected): assert ( eval( - Runtime("test", dict()), - Module("__repl__", dict()), + runtime, + Module("__repl__", [], dict()), Bindings("__root__", None), expr, ) @@ -29,11 +33,11 @@ def test_eval(expr, expected): ) -def test_hello_world(capsys): +def test_hello_world(capsys, runtime): assert ( eval( - Runtime("test", {}), - Module("__repl__", {Symbol("print"): print}), + runtime, + Module("__repl__", [], {Symbol("print"): print}), Bindings("__root__", None), Apply(Symbol("print"), Args(["hello, world"], {})), ) diff --git a/projects/lilith/test/python/test_parser.py b/projects/lilith/test/python/test_parser.py index 3b13657..6fa5b51 100644 --- a/projects/lilith/test/python/test_parser.py +++ b/projects/lilith/test/python/test_parser.py @@ -9,7 +9,6 @@ from lilith.parser import ( parser_with_transformer, Symbol, ) - import pytest diff --git a/projects/lilith/test/python/test_reader.py b/projects/lilith/test/python/test_reader.py index 4ccb1af..7898fa9 100644 --- a/projects/lilith/test/python/test_reader.py +++ b/projects/lilith/test/python/test_reader.py @@ -2,7 +2,6 @@ from lilith.parser import Apply, Args, Block, Symbol from lilith.reader import Module, read_buffer - import pytest @@ -12,7 +11,8 @@ import pytest ( """!def[main, lang[lil]]\nprint["hello, world"]\n""", Module( - "&buff", + Symbol("&buff"), + [], { Symbol("main"): Block( Apply(Symbol("lang"), Args([Symbol("lil")], {})), diff --git a/tools/python/requirements.txt b/tools/python/requirements.txt index f3d493b..8983238 100644 --- a/tools/python/requirements.txt +++ b/tools/python/requirements.txt @@ -27,6 +27,7 @@ lark==0.11.1 livereload==2.6.3 lxml==4.6.3 m2r==0.2.1 +Markdown==3.3.4 MarkupSafe==2.0.1 meraki==1.7.2 mirakuru==2.4.1