From ff18c055b8024c6fdeb96f709d97d28ff299ce40 Mon Sep 17 00:00:00 2001 From: Reid 'arrdem' McKenzie Date: Sat, 21 Aug 2021 21:17:48 -0600 Subject: [PATCH] Working lexical/module scope & hello world --- projects/lilith/src/lilith/hello.lil | 2 + projects/lilith/src/python/lilith/__main__.py | 45 +++--- .../lilith/src/python/lilith/interpreter.py | 88 ++++++++++-- projects/lilith/src/python/lilith/prelude.lil | 132 +++++++++--------- 4 files changed, 171 insertions(+), 96 deletions(-) create mode 100644 projects/lilith/src/lilith/hello.lil diff --git a/projects/lilith/src/lilith/hello.lil b/projects/lilith/src/lilith/hello.lil new file mode 100644 index 0000000..489ffef --- /dev/null +++ b/projects/lilith/src/lilith/hello.lil @@ -0,0 +1,2 @@ +!def[main, lil] +print["hello, world!"] diff --git a/projects/lilith/src/python/lilith/__main__.py b/projects/lilith/src/python/lilith/__main__.py index a6c01fe..7e0e03b 100644 --- a/projects/lilith/src/python/lilith/__main__.py +++ b/projects/lilith/src/python/lilith/__main__.py @@ -1,11 +1,12 @@ """The Lilith runner.""" +import logging import argparse from importlib.resources import read_text as resource_text import sys import traceback -from lilith.interpreter import Bindings, eval, Runtime +from lilith.interpreter import Bindings, eval as lil_eval, Runtime 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 @@ -15,6 +16,9 @@ from prompt_toolkit.styles import Style import yaml +log = logging.getLogger(__name__) + + STYLE = Style.from_dict( { # User input (default text). @@ -54,7 +58,7 @@ def repl(opts, args, runtime): continue try: - result = eval(runtime, module, Bindings("__root__", None), expr) + result = lil_eval(runtime, module, Bindings("__root__", None), expr) print_([("class:result", f"⇒ {result!r}")], style=STYLE) except Exception as e: traceback.print_exc() @@ -77,8 +81,7 @@ def batch(opts, args, runtime): # Register runtime.modules.update({mod.name: mod}) - print("DEBUG: batch mode") - print( + log.debug( yaml.dump( { "type": "runtime", @@ -96,7 +99,7 @@ def batch(opts, args, runtime): ) if main in mod.defs: - eval(runtime, mod, Bindings(main, None), mod.defs.get(main)) + lil_eval(runtime, mod, Bindings(main, None), mod.defs.get(main)) else: raise NameError(f"entry point {main} not found in {mod.name.name}") @@ -115,12 +118,21 @@ parser.add_argument( parser.add_argument( "--prelude", default="lilith.prelude", help="Select a module prelude." ) +parser.add_argument("-v", "--verbose", action="count", default=0) parser.add_argument("file", nargs="?", help="A file to start executing from") - if __name__ == "__main__": opts, args = parser.parse_known_args() + if opts.verbose == 0: + level = logging.WARN + elif opts.verbose == 1: + level = logging.INFO + elif opts.verbose > 1: + level = 0 + + logging.basicConfig(level=level) + # 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(":"): @@ -129,27 +141,26 @@ if __name__ == "__main__": # Building up a bootstrap interface for going out to many Python features. runtime = Runtime(Symbol("__runtime__"), Symbol(opts.prelude), {}) - def _lil(runtime=None, - module=None, - body=None, - name=None): + def py(runtime=None, module=None, expr=None, body=None, name=None): + """The implementation of the Python lang as an eval type.""" + return eval(body) + + def lil(runtime=None, module=None, expr=None, body=None, name=None): """The implementation of the Lilith lang as an eval type.""" expr = parse_expr(body) - return eval(runtime, module, Bindings(), expr) + return lil_eval(runtime, module, Bindings(), expr) bootstrap = Module( Symbol("lilith.bootstrap"), [], { - # The foundational meaning of lang[] - Symbol("lang"): lambda evaluator, body=None: evaluator(body), # The Python FFI escape hatch - Symbol("py"): lambda *args, body=None, **kwargs: eval(body), + Symbol("py"): py, # The Lilith self-interpreter - Symbol("lilith"): _lil, - }, + Symbol("lil"): lil, + }, ) - runtime.modules[Symbol(bootstrap.name)] = bootstrap + runtime.modules[bootstrap.name] = bootstrap prelude_mod = read_buffer( resource_text( ".".join(opts.prelude.split(".")[:-1]), opts.prelude.split(".")[-1] + ".lil" diff --git a/projects/lilith/src/python/lilith/interpreter.py b/projects/lilith/src/python/lilith/interpreter.py index a69fb81..a982124 100644 --- a/projects/lilith/src/python/lilith/interpreter.py +++ b/projects/lilith/src/python/lilith/interpreter.py @@ -2,10 +2,14 @@ A quick and dirty recursive interpreter for Lilith. """ +import logging import typing as t -from lilith.parser import Apply, Block, Symbol -from lilith.reader import Module +from lilith.parser import Apply, Block, Symbol, Args +from lilith.reader import Module, Import + + +log = logging.getLogger(__name__) class Runtime(t.NamedTuple): @@ -21,10 +25,10 @@ class BindingNotFound(KeyError): class Bindings(object): - def __init__(self, name=None, parent=None): + def __init__(self, name=None, bindings={}, parent=None): self._name = None self._parent = None - self._bindings = {} + self._bindings = bindings or {} def get(self, key): if key in self._bindings: @@ -38,24 +42,41 @@ class Bindings(object): raise BindingNotFound(f"No binding key {key}", self) -def lookup(runtime, mod, locals, name): +def lookup(ctx, mod, locals, name): """Implement resolving a name against multiple namespaces.""" + # First locals err = None try: + # Locals have already been evaluated return locals.get(name) except BindingNotFound as e: err = e + # Note that module globals, and code in other modules IS DEFERRED. We have + # to eval it before we can use it as a value. Eval IS RECURSIVE. + + # Then module scope if name in mod.defs: - return mod.defs.get(name) + return eval(ctx, mod, Bindings(), mod.defs.get(name)) + + # Then module imports + # Note that we include the prelude as a fallback import here + for i in mod.imports + [Import(ctx.prelude, {}, wild=True)]: + im = ctx.modules.get(i.src) + iname = i.names.get(name, name if i.wild else None) + if iname in im.defs: + return eval(ctx, im, Bindings(), im.defs.get(iname)) + + # Finally try a global reference + mod = Symbol(".".join(name.name.split(".")[:-1])) + name = Symbol(name.name.split(".")[-1]) + if mod := ctx.modules.get(mod): + if binding := mod.defs.get(name): + return eval(ctx, mod, Bindings(), binding) raise err or KeyError - # 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. @@ -68,9 +89,50 @@ def eval(ctx: Runtime, mod: Module, locals: Bindings, expr): # Pointedly not assert that the module is ACTUALLY in the runtime, # We're just going to assume this for convenience. + log.debug(f"eval[] {expr}") + if isinstance(expr, Symbol): return lookup(ctx, mod, locals, expr) + if isinstance(expr, Block): + # Blocks are INCREDIBLY magical to the interpreter, as they have body + # lines (understood to be body= as a kwarg) and any `lang[]` must be + # evaluated before the body can be returned. + + body = expr.body + expr = expr.app + # Do eval of a synthetic expression capturing the requisite context. + return eval( + ctx, + mod, + # In a synthetic context with all the non-evaluatable + # objects as locals + Bindings( + None, + { + Symbol("runtime"): ctx, + Symbol("module"): mod, + Symbol("expr"): expr, + Symbol("body"): body, + }, + locals, + ), + # Evaluate a synthetic expr using the locals to get kwargs + # through. + Apply( + expr, + Args( + [], + { + "runtime": Symbol("runtime"), + "module": Symbol("module"), + "expr": Symbol("expr"), + "body": Symbol("body"), + }, + ), + ), + ) + elif isinstance(expr, Apply): # Evaluate the call target fun = eval(ctx, mod, locals, expr.target) @@ -78,11 +140,9 @@ def eval(ctx: Runtime, mod: Module, locals: Bindings, expr): args = eval(ctx, mod, locals, expr.args.positionals) kwargs = eval(ctx, mod, locals, expr.args.kwargs) # Use Python's __call__ protocol + log.debug(f"eval[]; app; {fun}[*{args or []}, **{kwargs or dict()}]") 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] @@ -96,4 +156,4 @@ def eval(ctx: Runtime, mod: Module, locals: Bindings, expr): } else: - raise RuntimeError(f"Can't eval {expr}") + return expr diff --git a/projects/lilith/src/python/lilith/prelude.lil b/projects/lilith/src/python/lilith/prelude.lil index a859a5e..e560e38 100644 --- a/projects/lilith/src/python/lilith/prelude.lil +++ b/projects/lilith/src/python/lilith/prelude.lil @@ -1,129 +1,131 @@ -!import[lilith.bootstrap, [lang, py]] -!def[abs, lang[py]] +!import[lilith.bootstrap, [py, lil]] +!def[lil, lilith.bootstrap.lil] +lilith.bootstrap.lil +!def[abs, py] abs -!def[delattr, lang[py]] +!def[delattr, py] delattr -!def[hash, lang[py]] +!def[hash, py] hash -!def[set, lang[py]] +!def[set, py] set -!def[all, lang[py]] +!def[all, py] all -!def[dict, lang[py]] +!def[dict, py] dict -!def[min, lang[py]] +!def[min, py] min -!def[any, lang[py]] +!def[any, py] any -!def[dir, lang[py]] +!def[dir, py] dir -!def[hex, lang[py]] +!def[hex, py] hex -!def[next, lang[py]] +!def[next, py] next -!def[slice, lang[py]] +!def[slice, py] slice -!def[ascii, lang[py]] +!def[ascii, py] ascii -!def[divmod, lang[py]] +!def[divmod, py] divmod -!def[id, lang[py]] +!def[id, py] id -!def[object, lang[py]] +!def[object, py] object -!def[sorted, lang[py]] +!def[sorted, py] sorted -!def[bin, lang[py]] +!def[bin, py] bin -!def[enumerate, lang[py]] +!def[enumerate, py] enumerate -!def[input, lang[py]] +!def[input, py] input -!def[oct, lang[py]] +!def[oct, py] oct -!def[staticmethod, lang[py]] +!def[staticmethod, py] staticmethod -!def[bool, lang[py]] +!def[bool, py] bool -!def[eval, lang[py]] +!def[eval, py] eval -!def[int, lang[py]] +!def[int, py] int -!def[open, lang[py]] +!def[open, py] open -!def[str, lang[py]] +!def[str, py] str -!def[breakpoint, lang[py]] +!def[breakpoint, py] breakpoint -!def[exec, lang[py]] +!def[exec, py] exec -!def[isinstance, lang[py]] +!def[isinstance, py] isinstance -!def[ord, lang[py]] +!def[ord, py] ord -!def[sum, lang[py]] +!def[sum, py] sum -!def[bytearray, lang[py]] +!def[bytearray, py] bytearray -!def[filter, lang[py]] +!def[filter, py] filter -!def[issubclass, lang[py]] +!def[issubclass, py] issubclass -!def[pow, lang[py]] +!def[pow, py] pow -!def[super, lang[py]] +!def[super, py] super -!def[bytes, lang[py]] +!def[bytes, py] bytes -!def[float, lang[py]] +!def[float, py] float -!def[iter, lang[py]] +!def[iter, py] iter -!def[print, lang[py]] +!def[print, py] print -!def[tuple, lang[py]] +!def[tuple, py] tuple -!def[callable, lang[py]] +!def[callable, py] callable -!def[format, lang[py]] +!def[format, py] format -!def[len, lang[py]] +!def[len, py] len -!def[property, lang[py]] +!def[property, py] property -!def[type, lang[py]] +!def[type, py] type -!def[chr, lang[py]] +!def[chr, py] chr -!def[frozenset, lang[py]] +!def[frozenset, py] frozenset -!def[list, lang[py]] +!def[list, py] list -!def[range, lang[py]] +!def[range, py] range -!def[vars, lang[py]] +!def[vars, py] vars -!def[classmethod, lang[py]] +!def[classmethod, py] classmethod -!def[getattr, lang[py]] +!def[getattr, py] getattr -!def[locals, lang[py]] +!def[locals, py] locals -!def[repr, lang[py]] +!def[repr, py] repr -!def[zip, lang[py]] +!def[zip, py] zip -!def[compile, lang[py]] +!def[compile, py] compile -!def[map, lang[py]] +!def[map, py] map -!def[reversed, lang[py]] +!def[reversed, py] reversed -!def[complex, lang[py]] +!def[complex, py] complex -!def[hasattr, lang[py]] +!def[hasattr, py] hasattr -!def[max, lang[py]] +!def[max, py] max -!def[round, lang[py]] +!def[round, py] round