summaryrefslogtreecommitdiff
path: root/README.rst
blob: d4bc4c9fa13aa80a7f3b0177d2a44e1cebf979fd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
########
Actinide
########

.. image:: https://circleci.com/gh/ojacobson/actinide.svg?style=svg
    :target: https://circleci.com/gh/ojacobson/actinide

**An embeddable lisp for Python applications.**

I had `an application`_ in which the ability to extend the application from
within, safely, was valuable. None of the languages I reviewed met my criteria:

.. _an application: https://github.com/ojacobson/cadastre/

* `Lua`_ provides `primitives`_ which can be used to interact with the host
  environment, but no facility that guarantees that these tools are
  unavailable. I needed to be able to accept programs from hostile users
  without worrying that they'd overwrite files on the OS.

* `Python itself`_ has the same problem, but more so. Techniques for importing
  arbitrary code even in constrained Python execution environments are well
  known and, as far as I know, unfixable.

* `V8`_ is an attractive option, as it was originally built to evaluate
  Javascript functions in the browser. OS interaction is provided by the
  execution environment, rather than as part of the language's standard
  library. Leaving that out is easy.

  However, the problem space I was working in strongly pushed against using a
  language with no integers and with complex semicolon rules.

.. _Lua: https://www.lua.org
.. _primitives: https://www.lua.org/manual/5.3/manual.html#pdf-os.exit
.. _Python itself: https://python.org/
.. _V8: https://developers.google.com/v8/

So I wrote my own. This is that language.

This is a tiny lisp, along the lines of Peter Norvig's `lispy`_, designed to be
embedded within Python programs. It provides minimal safety features, but the
restricted set of builtins ensures that Actinide programs cannot gain access to
the outside context of the program. The worst they can do is waste CPU time,
fill up RAM, and drain your battery.

.. _lispy: http://norvig.com/lispy.html

************
Requirements
************

Actinide requires Python 3.6 or later.

************
Installation
************

::

    $ pip install actinide
    $ pip freeze > requirements.txt

Or, if you prefer, add ``actinide`` to your application's ``Pipfile`` or
``setup.py``.

*****************
Freestanding REPL
*****************

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. 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).

******************
Embedding Actinide
******************

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:

.. code:: python

    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:

.. code:: python

    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``:

.. code:: python

    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:

.. code:: python

    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:

.. code:: python

    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:

.. code:: python

    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:

.. code:: python

    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:

.. code:: python

    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:

.. code:: python

    session.run('''
        (begin
            ; 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.