aboutsummaryrefslogtreecommitdiffstats
path: root/core/base/op.moon
blob: b0a83dc5d16761a8719f424eefc5eac3117616b8 (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
----
-- Persistent expression Operator.
--
-- @classmod Op

deepcopy = (val) ->
  switch type val
    when 'number', 'string', 'boolean', 'nil'
      val
    when 'table'
      assert (not getmetatable {}), "state should only contain simple tables!"
      {(deepcopy k), (deepcopy v) for k,v in pairs val}
    else
      error "state cannot contain values of type '#{type val}'"

class Op
--- members
-- @section members

  do_yield = (table) ->
    for k, v in pairs table
      if v.__class
        coroutine.yield v
      else
        do_yield v
  --- yield all `Input`s from the (potentially nested) `inputs` table
  --
  -- @treturn iterator iterator over `inputs`
  all_inputs: => coroutine.wrap -> do_yield @inputs

  --- create a mutable copy of this Op.
  --
  -- Used to wrap insulate eval-cycles from each other. The copy does not have
  -- `inputs` set, since it is expected that this is (re)set in `setup`.
  --
  -- @treturn Op
  fork: =>
    out = if @out then @out\fork!
    state = if @state then deepcopy @state
    @@ out, state

  --- internal state of this Op.
  --
  -- This may be any simple Lua value, including Lua tables, as long as it has
  -- no metatables, multiple references/loops, userdata etc.
  --
  -- @tfield table state

  --- `Stream` instance representing this Op's computed output value.
  --
  -- Must be set to a `Stream` instance once `setup` finishes. Must not change
  -- type, be removed or replaced outside of `new` and `setup`. If it is a
  -- `ValueStream`, it should have a value assigned via `set` or the
  -- constructor once `tick` is called the first time. 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 an `Input.hot` with a `ValueStream`.
  --
  -- @tfield Stream out

  --- table containing `Input`s to this Op.
  --
  -- 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.
  --
  -- @tfield {Input,...} inputs

--- Op interface.
--
-- methods that have to be implemented by `Op` implementations.
-- @section interface

  --- construct a new instance.
  --
  -- The optional parameters `out` and `state` are used by `fork` to duplicate
  -- an instance. If the constructor is overriden, these parameters must be
  -- forwarded to the superconstructor unchanged.
  --
  -- @function new
  -- @classmethod
  -- @tparam ?Stream out `out`
  -- @tparam ?table state `state`

  --- 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 `base.match` to parse them, then delegate to
  -- `super:setup` to patch the `Input` instances.
  --
  -- @function setup
  -- @tparam {Result,...} inputs a sequence of `Result`s
  -- @tparam Scope scope the active scope

  --- handle incoming events and update `out` (optional).
  --
  -- Called once per frame if any `Input`s are dirty. Some `Input`s may have
  -- special behaviour immediately after `setup` that can cause them to become
  -- dirty at eval-time. In this case, an eval-time tick is executed. You can
  -- detect this using the `setup` parameter.
  --
  -- `tick` is called after `setup`. `tick` is not called immediately after
  -- `setup` if no `inputs` are dirty. Update `out` here.
  --
  -- @tparam bool setup whether this is an eval-time tick
  tick: =>

  --- called when the Op is destroyed (optional).
  destroy: =>

--- implementation utilities.
--
-- super-methods and utilities for use by implementations.
-- @section super

  --- 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.
  --
  -- @classmethod
  -- @tparam ?Stream out `out`
  -- @tparam ?table state `state`
  new: (@out, @state) =>

  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
  --- setup previous `inputs`, if any, with the new inputs, and write 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.
  --
  -- @tparam table inputs table of `Input`s
  setup: (inputs) =>
      old_inputs = @inputs
      @inputs = inputs
      do_setup old_inputs, @inputs

  do_unwrap = (value) ->
    if value.__class
      value\unwrap!
    else
      {k, do_unwrap v for k,v in pairs value}
  --- `\unwrap` all `Input`s in `@inputs` and return a table with the same
  -- shape.
  --
  -- @treturn table the values of all `Input`s
  unwrap_all: => do_unwrap @inputs

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

{
  :Op
}