summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOwen Jacobson <owen@grimoire.ca>2017-11-13 04:38:51 -0500
committerOwen Jacobson <owen@grimoire.ca>2017-11-13 04:38:51 -0500
commitd17c5d6354ab2a8a822107d59b0cf221edf99ee3 (patch)
tree5eb5348717411beee1cc6cf257884b3f7cb4c993
parent6ee8b48ce8f2189c23f8bf64bcf93e2210e67d26 (diff)
Re-add the repl, and a bunch of stdlib glue.
-rw-r--r--README.rst168
-rw-r--r--actinide/__init__.py47
-rw-r--r--actinide/builtin.py43
-rw-r--r--actinide/core.py14
-rw-r--r--actinide/evaluator.py2
-rw-r--r--actinide/ports.py8
-rw-r--r--actinide/reader.py2
-rw-r--r--actinide/stdlib.py60
-rw-r--r--actinide/types.py55
-rwxr-xr-xbin/actinide-repl33
-rw-r--r--primer.py37
-rw-r--r--setup.py1
12 files changed, 415 insertions, 55 deletions
diff --git a/README.rst b/README.rst
index 595c43e..9dfef88 100644
--- a/README.rst
+++ b/README.rst
@@ -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)
diff --git a/setup.py b/setup.py
index 7fc7629..8782f3f 100644
--- a/setup.py
+++ b/setup.py
@@ -4,6 +4,7 @@ setup(
name='actinide',
version='0.1',
packages=find_packages(),
+ scripts=['bin/actinide-repl'],
setup_requires=[
'pytest-runner',