summaryrefslogtreecommitdiff
path: root/docs/embedding.rst
blob: d179bafbaaa388c1de989b927f55e30093ad0b56 (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
##################
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-block:: 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-block:: 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-block:: 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-block:: 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-block:: python

    def example():
        return 5

    session.bind_fn(example)
    print(*session.run('(example)')) # prints "5"

To bind a function returning a tuple of results, call ``bind_builtin``. This
will automatically determine the name to bind to:

.. code-block:: 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. An equivalent set of methods, ``macro_bind``,
``macro_bind_void``, ``macro_bind_fn``, and ``macro_bind_builtin`` bind values
to entries in the top-level macro table, instead of the top-level environment,
and allow extension of the language's syntax.

Finally, Actinide can bind specially-crafted Python modules. If a module
contains a top-level symbol named ``An`` (for the informal chemical symbol for
the actinide series), it can be passed to the session's ``bind_module`` method.
The symbol must be bound to an instance of the ``Registry`` class from the
``actinide.builtin`` module:

.. code-block:: python

    from actinide.builtin import Registry
    An = Registry()

    five = An.bind('five', 5)

    @An.void
    def python_print(*args):
        print(*args)

    @An.fn
    def bitwise_and(a, b):
        return a & b

    @An.builtin
    def two_values():
        return 1, "Two"

    An.eval('''
        (begin
            (define (three-values) (values 1 2 3)))
    ''')

    # @An.macro_bind, @An.macro_void, @An.macro_fn, and @An.macro_builtin follow
    # the same pattern.

Going the other direction, values can be extracted from bindings in the session
using the ``get`` method:

.. code-block:: 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-block:: 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.

.. todo::

    Document the full public API.