aboutsummaryrefslogtreecommitdiffstats
path: root/core/base.moon
blob: 7962545ee19120e2982c26b7353e6cd825ec6be1 (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
-- base definitions for extensions
import Value from require 'core.value'
import Result from require 'core.result'
import match from require 'core.pattern'

unpack or= table.unpack

-- an incoming side-effect adapter, polled by the main event loop to pump
-- events into the dataflow graph.
--
-- subclasses must implement this interface:
--
-- :new() - construct a new instance
--
--   must prepare the instance for :dirty().
--
-- :tick() - poll for changes
--
--   called every frame by the event loop to update internal state.
--
-- :dirty() - whether this adapter requires processing
--
--   must return a boolean indicating whether `Op`s that refer to this instance
--   via `IOInput` should be notified (via `Op:tick()`). May be called multiple
--   multiple times. May be called before :tick() on the first frame after
--   construction.
--
class IO
  -- called in the main event loop
  tick: =>

  -- whether a tree update is necessary
  dirty: =>

-- a persistent expression Operator
--
-- subclasses must implement this interface:
--
-- :new() - construct a new instance
--
--   the super-constructor can be used to construct a `Value` instance in @out.
--
-- :setup(inputs, scope) - parse arguments and patch self
--
--   called once every eval-cycle. `inputs` is a list of `Result`s that are the
--   argument to this op. The `inputs` have to be wrapped in `Input` instances
--   to define update behaviour. Use `match` to parse them, then delegate to
--   super to patch the `Input` instances.
--
-- :tick(setup) - handle incoming events and update @out
--
--   called once per frame if any inputs are dirty. Some `Input`s (like
--   `ValueInput`) have special behaviour immediately after :setup(). You can
--   detect this using the `setup` parameter, which is true the first time
--   :tick() is called after :setup(). :tick() is not called immediately after
--   :setup() if no `@inputs` are dirty. Update @out here.
--
-- :destroy() - called when the Op is destroyed
--
-- .out - a `Value` instance representing this Op's computed output value.
--
--   @out must be set to a `Value` instance once :setup() finishes. @out must
--   not change type, be removed or replaced outside of :new() and :setup().
--   @out should have a value assigned via :set() or the `Value` constructor
--   once :tick(true) is called. If @out's value is not initialized in :new()
--   or :setup(), the implementation must make sure :tick(true) is called at
--   least on the first eval-cycle the Op goes through, e.g. by using a
--   `ValueInput`.
--
class Op
-- super-implementations for extensions
  -- if `type` is passed, an output stream is instantiated.
  -- if `init` is passed, the stream is initialized to that Lua value.
  -- it is okay not to use this and create the output stream in :setup() if the
  -- type is not known at this time.
  new: (type, init) =>
    if type
      @out = Value type, init

  -- setups previous @inputs, if any, with the new inputs, and writes them to
  -- `@inputs`. The `inputs` table can be nested with string or integer keys,
  -- but all leaf-entries must be `Input` instances. It must not contain loops
  -- or instances of other classes.
  setup: do
    do_setup = (old, cur) ->
      for k, cur_val in pairs cur
        old_val = old and old[k]

        -- are these inputs or nested tables?
        cur_plain = cur_val and not cur_val.__class
        old_plain = old_val and not old_val.__class

        if cur_plain and old_plain
          -- both are tables, recurse
          do_setup old_val, cur_val
        elseif not (cur_plain or old_plain)
          -- both are streams (or nil), setup them
          cur_val\setup old_val

    (inputs) =>
      old_inputs = @inputs
      @inputs = inputs
      do_setup old_inputs, @inputs

  tick: =>
  destroy: =>

-- utilities
  -- iterate over the (potentially nested) inputs table
  all_inputs: do
    do_yield = (table) ->
      for k, v in pairs table
        if v.__class
          coroutine.yield v
        else
          do_yield v

    => coroutine.wrap -> do_yield @inputs

  unwrap_all: do
    do_unwrap = (value) ->
      if value.__class
        value\unwrap!
      else
        {k, do_unwrap v for k,v in pairs value}

    => do_unwrap @inputs

  assert_types: (...) =>
    num = select '#', ...
    assert #@inputs >= num, "argument count mismatch"
    @assert_first_types ...

  assert_first_types: (...) =>
    num = select '#', ...
    for i = 1, num
      expect = select i, ...
      assert @inputs[i].type == expect, "expected argument #{i} of #{@} to be of type #{expect} but found #{@inputs[i]}"

-- static
  __tostring: => "<op: #{@@__name}>"
  __inherited: (cls) => cls.__base.__tostring = @__tostring

-- a builtin / special form / cell-evaluation strategy.
--
-- responsible for quoting/evaluating subexpressions, instantiating and patching
-- Ops updating the current Scope, etc. See core.builtin and core.invoke for
-- many examples.
class Action
  -- head: the (:eval'd) head of the Cell to evaluate (a Const)
  -- tag:  the Tag of the expression to evaluate
  new: (head, @tag) =>
    @patch head

  -- * eval args
  -- * perform scope effects
  -- * patch nested exprs
  -- * return runtime-tree value
  eval: (scope, tail) => error "not implemented"

  -- free resources
  destroy: =>

  -- update this instance for :eval() with new head
  -- if :patch() returns false, this instance is :destroy'ed and recreated
  -- instead must *not* return false when called after :new()
  -- only considered if Action types match
  patch: (head) =>
    if head == @head
      true

    @head = head

-- static
  -- find & patch the action for the expression with Tag 'tag' if it exists,
  -- and is compatible with the new Cell contents, otherwise instantiate it.
  -- register the action with the tag, evaluate it and return the Result
  @eval_cell: (scope, tag, head, tail) =>
    last = tag\last!
    compatible = last and
                 (last.__class == @) and
                 (last\patch head) and
                 last

    L\trace if compatible
      "reusing #{last} for #{tag} <#{@__name} #{head}>"
    else if last
      "replacing #{last} with new #{tag} <#{@__name} #{head}>"
    else
      "initializing #{tag} <#{@__name} #{head}>"

    action = if compatible
      tag\keep compatible
      compatible
    else
      last\destroy! if last
      with next = @ head, tag
        tag\replace next

    action\eval scope, tail

  __tostring: => "<#{@@__name} #{@head}>"
  __inherited: (cls) => cls.__base.__tostring = @__tostring

-- an ALV function definition
--
-- when called, expands its body with params bound to the fn arguments
-- (see core.invoke.fn-invoke)
class FnDef
  -- params: sequence of (:quote'd) symbols, each naming a function parameter
  -- body:   (:quote'd) expression the function evaluates to
  -- scope:  the lexical scope the function was defined in (closure)
  new: (@params, @body, @scope) =>

  __tostring: =>
    "(fn (#{table.concat [p\stringify! for p in *@params], ' '}) ...)"

-- an update scheduling policy for `Op`.
--
-- subclasses must implement this interface:
--
-- :new(value) - create an instance
--
--   `value` is either a `Value` or a `Result` instance and should be unwrapped.
--
-- :setup(prev) - copy state from old instance
--
--    called by `Op:setup()` with another `Input` instance or `nil` once this instance is
--    registered. Must prepare this instance for :dirty().
--
--- :dirty() - whether this input requires processing
--
--   must return a boolean indicating whether `Op`s that refer to this instance
--   should be notified (via `Op:tick()`).
--
-- :finish_setup() - leave setup state
--
--   called after the Op has completed (or skipped) its first `Op:tick()` after
--   `Op:setup()`. Must prepare this instance for dataflow operation.
--
class Input
  new: (value) =>
    assert value, "nil passed to Input: #{value}"
    @stream = switch value.__class
      when Result
        assert value.value, "Input from result without value!"
      when Value
        value
      else
        error "Input from unknown value: #{value}"

  setup: (previous) =>

  finish_setup: =>
  dirty: => @stream\dirty!
  unwrap: => @stream\unwrap!
  type: => @stream.type

  __call: => @stream\unwrap!
  __tostring: => "#{@@__name}:#{@stream}"
  __inherited: (cls) =>
    cls.__base.__call = @__call
    cls.__base.__tostring = @__tostring

-- Never marked dirty. Use this for input streams that are only read when
-- another input fires.
class ColdInput extends Input
  dirty: => false

-- Marked dirty for the setup-tick if old and new stream differ in current
-- value. This is the most common `Input` strategy. Should be used whenever a
-- value denotes state.
class ValueInput extends Input
  setup: (old) => @dirty_setup = not old or @stream != old.stream
  finish_setup: => @dirty_setup = false
  dirty: => @dirty_setup or @stream\dirty!

-- Only marked dirty if the input stream itself is dirty. Should be used
-- whenever a value denotes a momentary event or impulse.
class EventInput extends Input

-- Marked dirty when an IO object is dirty. Must be used for IO values.
class IOInput extends Input
  impure: true
  dirty: => @stream\unwrap!\dirty!

{
  :IO
  :Op
  :Action
  :FnDef

  :ValueInput, :EventInput, :IOInput, :ColdInput

  -- redundant exports, to keep anything an extension might need in one import
  :Value, :Result
  :match
}