aboutsummaryrefslogtreecommitdiffstats
path: root/docs/internals/writing_extensions.md
blob: af09e5490b011aaad1acb5055e25cd9d0d4b14db (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
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
# writing `alv` extensions

Extensions for `alv` are implemented in [Lua][lua] or [MoonScript][moonscript]
(which runs as Lua). When an `alv` module is [`(require)`][builtins-req]d,
alv looks for a Lua module `alv-lib.[module]`. You can simply add a new file
with extension `.lua` or `.moon` in the `alv-lib` directory of your alv
installation or somewhere else in your `LUA_PATH`.

To write extensions, a number of classes and utilities are required. All of
these are exported in the `base` module.

## alv values
In the alv runtime, values are represented as instances of one of the three
classes implementing the `Result` interface; `Constant`, `SigStream` or
`EvtStream`.

A `Result` contains a type, the "unwrapped" Lua value, and optional metadata.

### types
Different types are represented as instances of the `type.Type` interface.
Such types can be @{type.Primitive|Primitive} types (which are opaque to alv
user code), @{type.Array|Array}s or @{type.Struct|Struct}s.

@{type.Primitive|Primitive} types are identified simply as a string.
A primitive type should have a well-defined Lua equivalent that implementations
can expect when unwrapping a corresponding alv value. Here is how the types
used by alv and the standard library map to Lua values:

- `num`: Lua `number`
- `str`: Lua `string`
- `sym`: Lua `string`
- `bool`: Lua `boolean`
- `bang`: always Lua `true`
- `scope`: `Scope` instance
- `fndef`: `FnDef` instance
- `opdef`: class inheriting from `Op` or `PureOp`
- `builtin`: class inheriting from `Builtin`

New primitive types can be created by extensions to represent values that should be
opaque to other extensions and alv code. To avoid namespace collisions, such
primitive types should be prefixed with the extension name and a slash.
For example, the `love` extension uses the type `love/shape` internally.

To obtain primitive type instances easily, the `type.T` "magic table" is
provided. Simply indexing in this table will produce a cached
@{type.Primitive|Primitive} instance:

    import T from require 'alv.base'

    number_type = T.num
    shape_type = T['love/shape']

@{type.Array|Array}s and @{type.Struct|Struct}s are composite types that
contain other types.

Arrays contain a fixed number of elements of a single type. For example,
this code defines a "vec3" type that consists of three numbers:

    import T, Array from require 'alv.base'
    vec3 = Array 3, T.num

Structs contain a set of labelled values that can each have a different type.
This code snippet defines a "person" type with two keys, "name" and "age".

    import T, Struct from require 'alv.base'
    person = Struct { name: T.str, age: T.num }

`Type` instances provide shorthand methods to create instances of the three
*kinds* of `Result`:

    word = T.str\mk_const "hello" -- value required 
    odd_number = T.num\mk_sig 7   -- initial value (can be provided later)
    emails = T["email/message"]\mk_evt!

### metadata and documentation
Using `Constant.meta`, documentation metadata can also be attached to values.
This metadata is used for error messages, documentation generation and the
[`(doc)`][builtins-doc] builtin.

In the `meta` table `summary` is the only required key, but all of the
information that applies should be provided.

- `name`: the name of this export (for error reporting).
- `summary`: a one-line plain-text description of this entry. Should be
  capitalized and end with a period.
- `examples`: a table of strings, each of which is a short one-line code
  example illustrating the argument names for an Op.
- `description`: a longer markdown-formatted description of the functionality
  of this entry.

## module format
The lua module should return a `Result` which will be returned as the result
from [`(require)`][builtins-require]. In almost all cases, the return value
should be a `Scope` containing individual `Result`s that can be imported
together using [`(import)`][builtins-imp] and [`(import*)`][builtins-im_].

`Constant.meta` calls `Constant.wrap`, which will automatically turn raw tables
into `Scope`s and label other Lua primitive types correctly. 

    import Constant from require 'alv.base'

    -- define some values
    one = Constant.meta
      meta:
        name: 'one'
        summary: "the number one"
      value: 1

    two = Constant.meta
      meta:
        name: 'two'
        summary: "the number two"
      value: 2

    -- define and return a Constant of type "scope"
    -- that contains our exports
    Constant.meta
      meta:
        name: 'numbers'
        summary: "a module containing common numbers."
      value: { :one, :two }

## defining `Op`s
Most extensions will want to define a number of *Op*s to be used by the user.
They are implemented by deriving from the `Op` class and implementing at least
the `Op:setup` and `Op:tick` methods.

    import Constant, Op, Input, T, evt from require 'alv.base'

    total_sum = Constant.meta
      meta:
        name: 'total-sum'
        summary: "Keep a total of incoming numbers."
        examples: { '(total-sum num!)' }
        description: "Keep a total sum of incoming number events, extension-style."

      value: class extends Op
        setup: (inputs, scope) =>
          num = evt.num\match inputs

          super num: Inputs.hot num

          @state or= { total: 0 }
          @update_out '~', T.num, @state.total

        tick: =>
          @state.total += @inputs.num!
          @out\set @state.total

    Constant.meta
      meta:
        name: 'my-module'
        description: "This is my own awesome module."
      value: { 'total-sum': total_sum }

### Op:setup
`Op:setup` is called once every *eval cycle* to parse the Op's arguments, check
their types, choose the updating behaviour and define the output type.

The arguments to `:setup` are a list of inputs (each is a `Result` instance),
and the `Scope` the evaluation happened in. Ops generally shouldn't use the
scope, but might look up 'magic' dynamic symbols like `\*clock\*`.

#### argument parsing
Arguments should be parsed using `base.match`. `base.match.const`, `base.match.sig`
and `base.match.evt` are used to build complex patterns that can parse and
validate the Op arguments into complex structures (see the module documentation
for more information).

    import sig, evt from require 'alv.base'

    pattern = evt.bang + sig.str + sig.num*3 + -evt!
    { trig, str, numbers, optional } = pattern\match inputs

This example matches first an `EvtStream` of type `bang`, then a `SigStream`
of type `str`, followed by one, two or three `num`-values, and finally an
optional argument `EvtStream` of any type. `:match` will throw an error if it
couldn't (fully) match the arguments and otherwise return a structured mapping
of the inputs.

If there are more complex dependencies between arguments, it is recommended to
do as much of the parsing as possible using the `base.match` and then continue
manually. For invalid or missing arguments, `Error` instances should be thrown
using `error` or `assert`.

#### input setup
There are two types of inputs: `Input.hot` and `Input.cold`:

*Cold* inputs do not cause the Op to update when changes to the input stream
are made. They are useful to 'ignore' changes to inputs which are only relevant
when another input changed value. Imagine for example a `send-value-when` Op,
which sends a value only when a `bang!` input is live. This Op doesn't have to
update when the value changes, it's enough to update only when the trigger
input changes and simply read the value in that moment.

*Hot* inputs on the other hand mark the input stream as a dependency for the
Op. Depending on the type of `Result`, the semantics are a little different:

- For `SigStream`s, the Op updates whenever the current value changes. When
  an input stream is swapped out for another one at evaltime, but their values
  are momentarily equal, the input is not considered dirty.
- For `EvtStream`s and `IOStream`s, the Op updates whenever the stream is
  dirty. There is no special handling when the stream is swapped out at
  evaltime.

All `Result`s from the `inputs` argument that are taken into consideration
should be wrapped in an `Input` instance using either `Input.hot` or
`Input.cold`, and need to be passed to the `Op:setup` super implementation.
To illustrate with the `send-value-when` example:

    pattern = evt.bang + sig!
    setup: (inputs, scope) =>
      { trig, value } = pattern\match inputs

      super
        trig: Inputs.hot trig
        value: Inputs.cold value

`Op:setup` takes a table that can have any (even nested) shape you want, as
long as all 'leaf values' are `Input` instances. The following are both valid:

    super { (Inputs.hot trig), (Inputs.cold value) }

    super
      trigger: Inputs.hot trig
      values: { (Inputs.cold a), (Inputs.cold b), (Inputs.cold c) }

#### state and output setup
When `Op:setup` finishes, `Op.out` has to be set to a `Result` instance. The
instance can be created in `Op:setup`, or in an overridden constructor.
The same is true for `Op.state`, which is an (optional) raw table of state
that the operator keeps. `Op.state` can be nested, but must only contain
"simple" types, so that it can be duplicated. For more complex behaviour,
`Op:fork` can be overridden (see below).

When overriding the constructor, it is important to delegate to the `Op`
constructor and pass on all arguments using `...`. Keep in mind that the
Constructor is called not only when an Op is first created, but also to
sandbox changes before potentially rolling them back (more on this below).

There are three types of `Result`s that can be created for `Op.out`:

- `SigStream`s track *continuous values*. They can only have one value per
  tick, and downstream Ops will not update when a *SigStream* has been set
  to the same value it already had. They are updated using `SigStream:set`.
- `EvtStream`s transmit *momentary events*. They can transmit multiple events
  in a single tick. `EvtStream`s do not keep a value set on the last tick on
  the next tick. They are updated using `EvtStream:set`.
- `Constant`s do not change in-between evalcycles. Usually Ops do not output
  `Constant`s directly, as `SigStream`s outputs are automatically
  'downgraded' to `Constant`s when the Op has no reactive inputs.

It is best to only recreate `Op.out` and `Op.state` if that is absolutely
necessary (e.g. the output type has changed as a result of new inputs).
This is so that the Op continues running smoothly without discontinuities when
unrelated changes are made.

For this reason, in most cases `Op.state` should be set up using
`@state or= ...`, and `Op.out` with `Op.setup_state`:

    setup: =>
      @state or= 0
      @setup_out '~', T.num, 2

Sometimes `Op.state` depends on the output type and needs to be reset when that
changes. When the output was recreated, `Op.setup_out` returns `true`.

### Op:tick
`Op:tick` is called whenever any of the inputs are *dirty*. This is where the
Op's main logic will go. Generally here it should be checked which input(s)
changed, and then internal state and the output value may be updated.

To check whether inputs are dirty, the `Input:dirty` method can be called.
Inputs can then be unwrapped using `Input:unwrap`, but they can also be called
directly as a shorthand:

    tick: =>
      value = @inputs.value
      @out\set value + 1

Since `Op:tick` is only called when there is a dirty input, it's often not
necessary to check which inputs are dirty.

For brevity, the helper method `Op:unwrap_all` can be used to unwrap all inputs.
It returns a table matching the shape of `Op.inputs`:

    setup: (inputs) =>
      trig, a, b, c = pattern\match inputs
      super
        trigger: Inputs.hot trig
        values: { (Inputs.cold a), (Inputs.cold b), (Inputs.cold c) }

    tick: =>
      { :values, :values } = @unwrap_all!

      @out\set trigger + values[1] + values[2] + values[3]

When an Op is newly created or a *hot* input changes during evaluation,
`Op:tick` is invoked at evaltime to update `Op.out`. In this case,
`Op:tick` receives `true` as an argument. This is useful in rare cases where
`Op.out` is an `EvtStream` that is set both in `Op:setup` and `Op:tick`, and
collisions must be prevented.

### Op:fork
When a running file is re-evaluated, all Ops are *forked* before re-running
`Op:setup` on them. This is important, so that if an error occurs at any point
in the evaluation process, the forked Ops can be discarded while the original
Ops keep running without being affected by any changes that may have occured
as a result (e.g. changes to `Op.out` or `Op.state`).

To obtain a mutable copy of an Op, `Op:fork` is called.
By default, this does the following:

- fork `Op.out` (if it exists) using `Result:fork`
- deep-copy `Op.state` (if it exists)
- construct a new `Op` by invoking the constructor with these two arguments

If necessary, `Op:fork` can be overridden with custom logic. This can be useful
when it is necessary to synchronize state with external systems.

### IO Ops (`Op:poll`)
Regular Ops only update in response to Input changes, but there is a need to
source events from outside the system to make anything happen at all.

This is accomplished by *IO Op*s. *IO Op*s are `Op` classes that define the
`Op:poll` method. Whenever the program is idle, all IO Ops will have this
method called at a high rate.

When the method is called, an IO Op should check any external conditions and
return `true` if it wishes to trigger a tick. In this case it should also
write to an internally-created `Result` instance to mark itself as "dirty":

    class extends Op
      setup: =>
        super io: Input.hot T.bang\mk_evt!

      poll: =>
        -- query external state here
        if something_changed
          @inputs.io.result\set true
          true

      tick: =>
        @out\set external_state

## PureOps
[*Pure Operators*][ref-04-2-pop] share common semantics for input *kinds*.
To implement them, the base class `PureOp` is provided and takes care of any
boilerplate (argument parsing, kind validation, output setup).

To implement a PureOp, you need to specify three parts:

1. the argument types `PureOp.pattern`
2. the output type `PureOp:type`
3. the tick logic `PureOp:tick`


The argument types are specified as a class member `PureOp.pattern` with a
pattern value from `base.match`. `Op.inputs` (and therefore `Op:unwrap_all`'s
result) will follow the shape of the match:

    class PowOp extends PureOp
      pattern: any.num + any.num
      type: T.num
      tick: => @out\set math.pow unpack @unwrap_all!


The output type can either by specified directly as a class member, or
implemented as a method that returns the type value. If `PureOp:type` is a
method, it will receive the Op inputs as parsed by `PureOp.pattern`:

    class MakeArrayOp extends PureOp
      pattern: any!*0
      type: (args) => Array #args, args[1]\type!
      tick: =>
        args = @unwrap_all!
        @out\set args

`Op:tick` is implemented just like for regular Ops. Because of the PureOp
semantics, there is no need to check which inputs are dirty, so it's
recommended to use `Op:unwrap_all` to access the inputs.

### overriding `PureOp:setup`
For more control, it is possible to override `PureOp:setup`. When calling
`super`, the first argument should be a table of results that are treated
according `PureOp.pattern` as usual. The second parameter should be
forwarded. In the third parameter, extra `Input`s can be specified that will
be merged into `Op.inputs`:

    class LogAll extends PureOp
      pattern: any.num*0

      full_pattern = -sig.str + any.num*0
      setup: (inputs, scope) =>
        { name, values } = full_pattern\match inputs
        super values, scope, {
          name: Input.cold name or scope\get '*name*'
        }

      tick: =>
        args = @unwrap_all!
        for i=1,#args
          print args.name, args[i]

## defining `Builtin`s
Builtins are more powerful than Ops because they control whether, how and
when their arguments are evaluated. They roughly correspond to *macros* in Lisps.
There is less of a concrete guideline for implementing Builtins because there
are a lot more options, and it really depends a lot on what the Builtin should
achieve. Nevertheless, a good starting point is to read the `Builtin` class
documentation, take a look at `Builtin`s in `alv/builtins.moon` and get
familiar with the relevant internal interfaces (especially `AST`, `Result`, and
`Scope`).

[lua]:          https://www.lua.org/
[moonscript]:   http://moonscript.org/
[builtins-req]: ../../reference/builtins.html#require
[builtins-imp]: ../../reference/builtins.html#import
[builtins-im_]: ../../reference/builtins.html#import*
[builtins-doc]: ../../reference/builtins.html#doc
[modules-midi]: ../../reference/midi.html
[ref-04-2-pop]: ../../reference/04-2_pure-operators.html