diff options
| author | Owen Jacobson <owen@grimoire.ca> | 2017-11-13 04:38:51 -0500 |
|---|---|---|
| committer | Owen Jacobson <owen@grimoire.ca> | 2017-11-13 04:38:51 -0500 |
| commit | d17c5d6354ab2a8a822107d59b0cf221edf99ee3 (patch) | |
| tree | 5eb5348717411beee1cc6cf257884b3f7cb4c993 | |
| parent | 6ee8b48ce8f2189c23f8bf64bcf93e2210e67d26 (diff) | |
Re-add the repl, and a bunch of stdlib glue.
| -rw-r--r-- | README.rst | 168 | ||||
| -rw-r--r-- | actinide/__init__.py | 47 | ||||
| -rw-r--r-- | actinide/builtin.py | 43 | ||||
| -rw-r--r-- | actinide/core.py | 14 | ||||
| -rw-r--r-- | actinide/evaluator.py | 2 | ||||
| -rw-r--r-- | actinide/ports.py | 8 | ||||
| -rw-r--r-- | actinide/reader.py | 2 | ||||
| -rw-r--r-- | actinide/stdlib.py | 60 | ||||
| -rw-r--r-- | actinide/types.py | 55 | ||||
| -rwxr-xr-x | bin/actinide-repl | 33 | ||||
| -rw-r--r-- | primer.py | 37 | ||||
| -rw-r--r-- | setup.py | 1 |
12 files changed, 415 insertions, 55 deletions
@@ -66,13 +66,11 @@ Or, if you prefer, add ``actinide`` to your application's ``Pipfile`` or Freestanding REPL ***************** -**Note: this section is presently incorrect - the ``actinide-repl`` command -doesn't exist.** - The Actinide interpreter can be started interactively using the ``actinide-repl`` command. In this mode, Actinide forms can be entered interactively. The REPL will immediately evaluate each top-level form, then -print the result of that evaluation. +print the result of that evaluation. The environment is persisted from form to +form, to allow interactive definitions. To exit the REPL, type an end-of-file (Ctrl-D on most OSes, Ctrl-Z on Windows). @@ -80,7 +78,167 @@ To exit the REPL, type an end-of-file (Ctrl-D on most OSes, Ctrl-Z on Windows). Embedding Actinide ****************** -WIP +Actinide is designed to be embedded into larger Python programs. It's possible +to call into Actinide, either by providing code to be evaluated, or by +obtaining builtin functions and procedures from Actinide and invoking them. + +The ``Session`` class is the basic building block of an Actinide integration. +Creating a session creates a number of resources associated with Actinide +evaluation: a symbol table for interning symbols, and an initial top-level +environment to evaluate code in, pre-populated with the Actinide standard +library. + +Executing Actinide programs in a session consists of two steps: reading the +program in from a string or an input port, and evaluating the resulting forms. +The following example illustrates a simple infinite loop: + +:: + + import actinide + + session = actinide.Session() + program = session.read(''' + (begin + ; define the factorial function + (define (factorial n) + (fact n 1)) + + ; define a tail-recursive factorial function + (define (fact n a) + (if (= n 1) + a + (fact (- n 1) (* n a)))) + + ; call them both + (factorial 100)) + ''') + + # Compute the factorial of 100 + result = session.eval(program) + +As a shorthand for this common sequence of operations, the Session exposes a +``run`` method: + +:: + + print(*session.run('(factorial 5)')) # prints "120" + +Callers can inject variables, including new builtin functions, into the initial +environment using the ``bind``, ``bind_void``, ``bind_fn``, and +``bind_builtin`` methods of the session. + +To bind a simple value, or to manually bind a wrapped builtin, call +``session.bind``: + +:: + + session.bind('var', 5) + print(*session.run('var')) # prints "5" + +To bind a function whose return value should be ignored, call ``bind_void``. +This will automatically determine the name to bind the function to: + +:: + + session.bind_void(print) + session.run('(print "Hello, world!")') # prints "Hello, world!" using Python's print fn + +To bind a function returning one value (most functions), call ``bind_fn``. This +will automatically determine the name to bind to: + +:: + + def example(): + return 5 + + session.bind_fn(example) + print(*session.run('(example)')) # prints "5" + +Finally, to bind a function returning a tuple of results, call +``bind_builtin``. This will automatically determine the name to bind to: + +:: + + def pair(): + return 1, 2 + + session.bind_builtin(pair) + print(*session.run('(pair)')) # prints "1 2" + +Actinide functions can return zero, one, or multiple values. As a result, the +``result`` returned by ``session.eval`` is a tuple, with one value per result. + +Actinide can bind Python functions, as well as bound and unbound methods, and +nearly any other kind of callable. Under the hood, Actinide uses a thin adapter +layer to map Python return values to Actinide value lists. The ``bind_void`` +helper ultimately calls that module's ``wrap_void`` to wrap the function, and +``bind_fn`` calls ``wrap_fn``. (Tuple-returning functions do not need to be +wrapped.) If you prefer to manually bind functions using ``bind``, they must be +wrapped appropriately. + +Finally Actinide can bind specially-crafted Python modules. If a module +contains top-level symbols named ``ACTINIDE_BINDINGS``, ``ACTINIDE_VOIDS``, +``ACTINIDE_FNS``, and/or ``ACTINIDE_BUILTINS``, it can be passed to the +session's ``bind_module`` method. The semantics of each symbol are as follows: + +* ``ACTINIDE_BINDINGS`` is a list of name, value pairs. Each binding binding + will be installed verbatim, without any function mangling, as if by + ``session.bind``. + +* ``ACTINIDE_VOIDS``, ``ACTINIDE_FNS``, and ``ACTINIDE_BUILTINS`` are lists of + function objects. Each will be bound as if by the corresponding + ``session.bind_void``, ``session.bind_fn``, or ``session.bind_builtin`` + method. + +The ``actinide.builtin`` module contains a helper function, ``make_registry``, +which can simplify construction of these fields: + +:: + + from actinide.builtin import make_registry + ACTINIDE_BINDINGS, ACTINIDE_VOIDS, ACTINIDE_FNS, ACTINIDE_BUILTINS, bind, void, fn, builtin = make_registry() + + five = bind('five', 5) + + @void + def python_print(*args): + print(*args) + + @fn + def bitwise_and(a, b): + return a & b + + @builtin + def two_values(): + return 1, "Two" + +Going the other direction, values can be extracted from bindings in the session +using the ``get`` method: + +:: + + session.run('(define x 8)') + print(session.get('x')) # prints "8" + +If the extracted value is a built-in function or an Actinide procedure, it can +be invoked like a Python function. However, much like ``eval`` and ``run``, +Actinide functions returne a tuple of results rather than a single value: + +:: + + session.run(''' + ; Set a variable + (define x 5) + + ; Define a function that reads the variable + (define (get-x) x) + ''') + + get_x = session.get('get-x') + print(*get_x()) # prints "5" + +This two-way binding mechanism makes it straightforward to define interfaces +between Actinide and the target domain. ********************************* The Actinide Programming Language diff --git a/actinide/__init__.py b/actinide/__init__.py index 0278cd8..338eace 100644 --- a/actinide/__init__.py +++ b/actinide/__init__.py @@ -1,20 +1,27 @@ -from . import builtin, ports, symbol_table, reader, expander, evaluator, types +from . import builtin, core, stdlib, ports, symbol_table, reader, expander, evaluator, types -class Session(object): +# A session with a minimal standard library. +class BaseSession(object): def __init__(self): self.symbols = symbol_table.SymbolTable() self.environment = evaluator.Environment() + self.core_builtins() + self.standard_library() def read(self, port): if types.string_p(port): port = ports.string_to_input_port(port) - form = reader.read(port, self.symbols) - return expander.expand(form, self.symbols) + return reader.read(port, self.symbols) def eval(self, form): + form = expander.expand(form, self.symbols) cps = evaluator.eval(form, self.environment, self.symbols, None) return evaluator.run(cps) + def run(self, port): + form = self.read(port) + return self.eval(form) + def bind(self, symb, value): self.environment[self.symbol(symb)] = value @@ -32,6 +39,14 @@ class Session(object): self.bind(symb, fn) return symb + def bind_module(self, module): + for name, binding in getattr(module, 'ACTINIDE_BINDINGS', []): + self.bind(name, binding) + for fn in getattr(module, 'ACTINIDE_FNS', []): + self.bind_fn(fn) + for builtin in getattr(module, 'ACTINIDE_BUILTINS', []): + self.bind_builtin(builtin) + def get(self, symb): symb = self.symbol(symb) return self.environment.find(symb) @@ -40,3 +55,27 @@ class Session(object): if types.string_p(symb): symb = types.symbol(symb, self.symbols) return symb + + def core_builtins(self): + self.bind_module(core) + + def standard_library(self): + pass + +class Session(BaseSession): + def standard_library(self): + @self.bind_fn + def symbol(val): + return types.symbol(val, self.symbols) + @self.bind_fn + def read(port): + return reader.read(port, self.symbols) + @self.bind_builtin + def eval(form): + return self.eval(form) + @self.bind_fn + def expand(form): + return expander.expand(form, self.symbols) + self.bind_module(types) + self.bind_module(stdlib) + self.bind_module(ports) diff --git a/actinide/builtin.py b/actinide/builtin.py index 06623b3..68ed698 100644 --- a/actinide/builtin.py +++ b/actinide/builtin.py @@ -2,8 +2,24 @@ from functools import wraps +dunder_names = { + '__add__': '+', + '__sub__': '-', + '__mul__': '*', + '__floordiv__': '/', + '__eq__': '=', + '__ne__': '!=', + '__lt__': '<', + '__le__': '<=', + '__gt__': '>', + '__ge__': '>', +} + # Derives the lisp name of a python function or method handle, as follows: # +# * Python dunder names are translated to their corresponding operator or +# symbol usind the ``dunder_names`` table. +# # * A trailing '_p' in the Python name is converted into a question mark in the # lisp name. # @@ -17,6 +33,9 @@ def lisp_name(fn): if name == '<lambda>': return None + if name in dunder_names: + return dunder_names[name] + # Trailing _p is a predicate, translate to ? if name.endswith('_p'): name = name[:-2] + '?' @@ -42,3 +61,27 @@ def wrap_fn(fn): def wrapper(*args): return fn(*args), return wrapper + +def make_registry(): + bindings = [] + voids = [] + fns = [] + builtins = [] + + def bind(name, val): + bindings.append((name, val)) + return val + + def void(f): + voids.append(f) + return f + + def fn(f): + fns.append(f) + return f + + def builtin(f): + builtins.append(f) + return f + + return bindings, voids, fns, builtins, bind, void, fn, builtin diff --git a/actinide/core.py b/actinide/core.py new file mode 100644 index 0000000..1b4c800 --- /dev/null +++ b/actinide/core.py @@ -0,0 +1,14 @@ +# Core functions +from .builtin import make_registry + +ACTINIDE_BINDINGS, ACTINIDE_VOIDS, ACTINIDE_FNS, ACTINIDE_BUILTINS, bind, void, fn, builtin = make_registry() + +@fn +def begin(*args): + if args: + return args[-1] + return None + +@builtin +def values(*args): + return args diff --git a/actinide/evaluator.py b/actinide/evaluator.py index 49587e2..747a428 100644 --- a/actinide/evaluator.py +++ b/actinide/evaluator.py @@ -136,7 +136,7 @@ def invoke(continuation): def eval(value, environment, symbols, continuation): if t.symbol_p(value): return symbol(value, environment, continuation) - if not t.list_p(value): + if t.nil_p(value) or not t.list_p(value): return literal(value, continuation) # Special forms (all of which begin with a special symbol, discarded here) if t.head(value) == symbols['if']: diff --git a/actinide/ports.py b/actinide/ports.py index 6f419e5..2c116e8 100644 --- a/actinide/ports.py +++ b/actinide/ports.py @@ -1,5 +1,9 @@ import io +from .builtin import make_registry + +ACTINIDE_BINDINGS, ACTINIDE_VOIDS, ACTINIDE_FNS, ACTINIDE_BUILTINS, bind, void, fn, builtin = make_registry() + # ## PORTS # # A port is a handle which characters can either be read from (an "input port") @@ -36,19 +40,23 @@ class Port(object): # Read at least 1 and up to ``n`` characters from a port. This consumes them # from the port: they are no longer available to future peeks or reads. ``n`` # must be strictly positive. +@fn def read_port(port, n): return port.read(n) # Read all remaining input from a port, consuming it. +@fn def read_port_fully(port): return port.read_fully() # Read at least 1 and up to ``n`` characters from a port, without consuming # them. They will be available on future peeks and reads. ``n`` must be strictly # positive. +@fn def peek_port(port, n): return port.peek(n) # Create an input port from a string. +@fn def string_to_input_port(string): return Port(io.StringIO(string)) diff --git a/actinide/reader.py b/actinide/reader.py index 06d774c..0955309 100644 --- a/actinide/reader.py +++ b/actinide/reader.py @@ -23,7 +23,7 @@ class SyntaxError(Exception): def read(port, symbols): head = read_token(port) if head is None: - raise SyntaxError("Unexpected end of input") + return None if head == ')': raise SyntaxError("Unexpected ')'") if head == '(': diff --git a/actinide/stdlib.py b/actinide/stdlib.py new file mode 100644 index 0000000..6401863 --- /dev/null +++ b/actinide/stdlib.py @@ -0,0 +1,60 @@ +import operator as op +import functools as f + +from .types import * +from .builtin import make_registry + +ACTINIDE_BINDINGS, ACTINIDE_VOIDS, ACTINIDE_FNS, ACTINIDE_BUILTINS, bind, void, fn, builtin = make_registry() + +@fn +def __add__(*vals): + return f.reduce(op.add, vals) + +@fn +def __sub__(val, *vals): + if vals: + return f.reduce(op.sub, (val, *vals)) + return op.neg(val) + +@fn +def __mul__(*vals): + return f.reduce(op.mul, vals) + +@fn +def __floordiv__(*vals): + div = op.floordiv + if any(decimal_p(val) for val in vals): + div = op.truediv + return f.reduce(div, vals) + +@fn +def __eq__(a, b): + return op.eq(a, b) + +@fn +def __ne__(a, b): + return op.ne(a, b) + +@fn +def __lt__(a, b): + return op.lt(a, b) + +@fn +def __le__(a, b): + return op.le(a, b) + +@fn +def __gt__(a, b): + return op.gt(a, b) + +@fn +def __ge__(a, b): + return op.ge(a, b) + +@fn +def eq_p(a, b): + return op.is_(a, b) + +@fn +def equal_p(a, b): + return op.eq(a, b) diff --git a/actinide/types.py b/actinide/types.py index 54a0c04..13f1368 100644 --- a/actinide/types.py +++ b/actinide/types.py @@ -9,19 +9,25 @@ from decimal import Decimal, InvalidOperation from . import evaluator as e from .environment import * +from .builtin import make_registry + +ACTINIDE_BINDINGS, ACTINIDE_VOIDS, ACTINIDE_FNS, ACTINIDE_BUILTINS, bind, void, fn, builtin = make_registry() # ### Nil # # Nil is a type with a single value, usually taken to denote no value. -nil = None +nil = bind('nil', None) +@fn def nil_p(value): return value is None +@fn def read_nil(value): return nil +@fn def display_nil(value): return '()' @@ -29,12 +35,14 @@ def display_nil(value): # # The true and false values. -true = True -false = False +true = bind('#t', True) +false = bind('#f', False) +@fn def boolean_p(value): - return value in (true, false) + return value is true or value is false +@fn def read_boolean(value): if value == '#t': return true @@ -42,6 +50,7 @@ def read_boolean(value): return false return None +@fn def display_boolean(value): return '#t' if value else '#f' @@ -51,17 +60,20 @@ def display_boolean(value): # These are fixed-precision numbers with no decimal part, obeying common notions # of machine integer arithmetic. They support large values. -integer = int +integer = bind('integer', int) +@fn def integer_p(value): return isinstance(value, integer) +@fn def read_integer(value): try: return integer(value) except ValueError: return nil +@fn def display_integer(value): return str(value) @@ -69,17 +81,20 @@ def display_integer(value): # # These are variable-precision numbers, which may have a decimal part. -decimal = Decimal +decimal = bind('decimal', Decimal) +@fn def decimal_p(value): return isinstance(value, decimal) +@fn def read_decimal(value): try: return decimal(value) except InvalidOperation: return nil +@fn def display_decimal(value): return str(value) @@ -87,17 +102,20 @@ def display_decimal(value): # # Sequences of characters. -string = str +string = bind('string', str) +@fn def string_p(value): return not symbol_p(value) and isinstance(value, string) +@fn def read_string(value): value = value[1:-1] value = value.replace('\\"', '"') value = value.replace('\\\\', '\\') return value +@fn def display_string(value): value = value.replace('\\', '\\\\') value = value.replace('"', '\\"') @@ -118,15 +136,19 @@ class Symbol(object): def __repr__(self): return f'Symbol({repr(self.value)})' +# bind manually, fix the symbol table def symbol(string, symbol_table): return symbol_table[string] +@fn def symbol_p(value): return isinstance(value, Symbol) +@fn def read_symbol(value, symbol_table): return symbol(value, symbol_table) +@fn def display_symbol(value): return str(value) @@ -136,18 +158,23 @@ def display_symbol(value): Cons = namedtuple('Cons', 'head tail') +@fn def cons(head, tail): return Cons(head, tail) +@fn def cons_p(value): return isinstance(value, Cons) +@fn def head(cons): return cons.head +@fn def tail(cons): return cons.tail +@fn def display_cons(value): parts = [] while cons_p(value): @@ -160,6 +187,7 @@ def display_cons(value): # ### Lists +@fn def list(*elems): if elems: head, *tail = elems @@ -167,6 +195,7 @@ def list(*elems): else: return nil +@fn def list_p(value): return nil_p(value) or cons_p(value) and list_p(tail(value)) @@ -197,10 +226,20 @@ class Procedure(object): def continuation(self, environment, continuation): return e.eval(self.body, environment, self.symbols, continuation) +@fn def procedure_p(value): return callable(value) +@fn +def display_procedure(proc): + if isinstance(proc, Procedure): + formals = ' '.join(display(formal) for formal in proc.formals) + body = display(proc.body) + return f'<procedure: (lambda ({formals}) {body})>' + return f'<builtin: {proc.__name__}>' + # ### General-purpose functions +@fn def display(value): if cons_p(value): return display_cons(value) @@ -216,3 +255,5 @@ def display(value): return display_integer(value) if decimal_p(value): return display_decimal(value) + if procedure_p(value): + return display_procedure(value) diff --git a/bin/actinide-repl b/bin/actinide-repl new file mode 100755 index 0000000..de19617 --- /dev/null +++ b/bin/actinide-repl @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 + +import sys + +import actinide as a +import actinide.ports as ap +import actinide.types as at + +def repl(session, port): + while True: + try: + sys.stdout.write("> ") + sys.stdout.flush() + form = session.read(port) + if form is None: + print() + return 0 + results = session.eval(form) + print(*(at.display(result) for result in results)) + except Exception as e: + print(e) + except KeyboardInterrupt: + print() + print("(Interrupted)") + +def main(): + port = ap.Port(sys.stdin) + session = a.Session() + + return repl(session, port) + +if __name__ == '__main__': + sys.exit(main()) diff --git a/primer.py b/primer.py deleted file mode 100644 index b223550..0000000 --- a/primer.py +++ /dev/null @@ -1,37 +0,0 @@ -import actinide -import actinide.types as t -import actinide.builtin as b -import actinide.evaluator as e - -session = actinide.Session() -program = session.read(""" -(begin - 1 - 1.0 - "Hello" - (define (a b) - (values 1 2.2 "three" a b)) - (define (pp) (pp)) - - (print (a "foo")) - (print (eval (list (symbol "a") "bar"))) - (print 0 (values 1 2 3) 4 5) - (pp)) -""") - -def begin(*args): - if args: - return args[-1] - return None - -def values(*args): - return args - -session.bind_builtin(values) -session.bind_void(print) -session.bind_fn(begin) -session.bind_fn(t.list) -session.bind_fn(session.symbol) -session.bind_builtin(session.eval) - -session.eval(program) @@ -4,6 +4,7 @@ setup( name='actinide', version='0.1', packages=find_packages(), + scripts=['bin/actinide-repl'], setup_requires=[ 'pytest-runner', |
