aboutsummaryrefslogtreecommitdiffstats
path: root/alv/base
diff options
context:
space:
mode:
authors-ol <s-ol@users.noreply.github.com>2020-04-13 18:40:35 +0000
committers-ol <s-ol@users.noreply.github.com>2020-04-14 08:46:54 +0000
commit1a8debe87762072b3a63b769aa515ebf63b4c70d (patch)
tree33b42866307d11d2c0878b23e0ec0bb8925d3fcc /alv/base
parentspec base.match __tostring (diff)
downloadalive-1a8debe87762072b3a63b769aa515ebf63b4c70d.tar.gz
alive-1a8debe87762072b3a63b769aa515ebf63b4c70d.zip
move into proper Lua module (`alv`)
Diffstat (limited to 'alv/base')
-rw-r--r--alv/base/builtin.moon96
-rw-r--r--alv/base/fndef.moon26
-rw-r--r--alv/base/init.moon39
-rw-r--r--alv/base/input.moon146
-rw-r--r--alv/base/match.moon302
-rw-r--r--alv/base/op.moon168
6 files changed, 777 insertions, 0 deletions
diff --git a/alv/base/builtin.moon b/alv/base/builtin.moon
new file mode 100644
index 0000000..fd36c69
--- /dev/null
+++ b/alv/base/builtin.moon
@@ -0,0 +1,96 @@
+----
+-- Builtin / Special Form evaluation Strategy (`builtin`).
+--
+-- Responsible for quoting/evaluating subexpressions, instantiating and setting
+-- up `Op`s, updating the current `Scope`, etc.
+-- See `builtin` and `invoke` for examples.
+--
+-- @classmod Builtin
+
+class Builtin
+--- Builtin interface.
+--
+-- methods that have to be implemented by `Builtin` implementations.
+-- @section interface
+
+ --- create a new instance.
+ --
+ -- @tparam Cell cell the Cell to evaluate
+ -- @tparam Value head the (`AST:eval`d) `head` of the Cell to evaluate
+ new: (@cell, @head) =>
+ @tag = @cell.tag
+ @tag\register @
+
+ --- perform the actual evaluation.
+ --
+ -- Implementations should:
+ --
+ -- - eval or quote `tail` values
+ -- - perform scope effects
+ -- - wrap all child-results
+ --
+ -- @tparam Scope scope the active scope
+ -- @tparam {AST,...} tail the arguments to this expression
+ -- @treturn Result the result of this evaluation
+ eval: (scope, tail) => error "not implemented"
+
+ --- free resources
+ destroy: =>
+
+ --- setup or copy state from previous instance of same type.
+ --
+ -- `prev` is only passed if Builtin types of prev and current expression match.
+ -- Otherwise, or when no previous expression exists, `nil` is passed.
+ --
+ -- @tparam ?Builtin prev the previous Builtin instance
+ setup: (prev) =>
+
+ --- the `Cell` this Builtin was created for.
+ -- @tfield Cell cell
+
+ --- the evaluated head of `cell`.
+ -- @tfield AST head
+
+ --- the identity of `cell`.
+ -- @tfield Tag tag
+
+--- static functions
+-- @section static
+
+ --- create and setup a `Builtin` for a given tag, then evaluate it.
+ --
+ -- Create a new instance using `tag` and `head` and call `setup` on it.
+ -- If a previous instance with the same `tag` exists and has the same `head`,
+ -- it pass it to `setup`. Register the `Builtin` with `tag`, evaluate it
+ -- and return the `Result`.
+ --
+ -- @tparam Cell cell the `Cell` being evaluated
+ -- @tparam Scope scope the active scope
+ -- @tparam Value head the (`AST:eval`d) head of the `Cell` being evaluated
+ -- @treturn Result the result of evaluation
+ @eval_cell: (cell, scope, head) =>
+ last = cell.tag\last!
+ compatible = last and (last.__class == @) and last.head == head
+
+ L\trace if compatible
+ "reusing #{last} for #{cell.tag} <#{@__name} #{head}>"
+ else if last
+ "replacing #{last} with new #{cell.tag} <#{@__name} #{head}>"
+ else
+ "initializing #{cell.tag} <#{@__name} #{head}>"
+
+ builtin = @ cell, head
+ if compatible
+ builtin\setup last
+ else
+ last\destroy! if last
+ builtin\setup nil
+
+ builtin\eval scope, cell\tail!
+
+ __tostring: => "<#{@@__name} #{@head}>"
+ __inherited: (cls) => cls.__base.__tostring = @__tostring
+
+{
+ :Builtin
+}
diff --git a/alv/base/fndef.moon b/alv/base/fndef.moon
new file mode 100644
index 0000000..b79ad85
--- /dev/null
+++ b/alv/base/fndef.moon
@@ -0,0 +1,26 @@
+----
+-- user-function definition (`fndef`).
+--
+-- When called, expands to its body with params bound to the fn arguments (see
+-- `invoke.fn_invoke`).
+--
+-- @classmod FnDef
+
+class FnDef
+--- static functions
+-- @section static
+
+ --- create a new instance
+ --
+ -- @classmethod
+ -- @tparam {Value,...} params (`AST:quote`d) naming the function parameters
+ -- @tparam AST body (`AST:quote`d) expression the function evaluates to
+ -- @tparam Scope scope the lexical scope the function was defined in (closure)
+ new: (@params, @body, @scope) =>
+
+ __tostring: =>
+ "(fn (#{table.concat [p\stringify! for p in *@params], ' '}) ...)"
+
+{
+ :FnDef
+}
diff --git a/alv/base/init.moon b/alv/base/init.moon
new file mode 100644
index 0000000..c37d85c
--- /dev/null
+++ b/alv/base/init.moon
@@ -0,0 +1,39 @@
+----
+-- Base definitions for extensions.
+--
+-- This module exports the following classes and tables that extension modules
+-- may need:
+--
+-- @module base
+-- @see Op
+-- @see Builtin
+-- @see FnDef
+-- @see Input
+-- @see base.match.val
+-- @see base.match.evt
+-- @see ValueStream
+-- @see EventStream
+-- @see IOStream
+-- @see Result
+-- @see Error
+
+import Op from require 'alv.base.op'
+import Builtin from require 'alv.base.builtin'
+import FnDef from require 'alv.base.fndef'
+import Input from require 'alv.base.input'
+import val, evt from require 'alv.base.match'
+import ValueStream, EventStream, IOStream from require 'alv.stream'
+import Result from require 'alv.result'
+import Error from require 'alv.error'
+
+{
+ :Op
+ :Builtin
+ :FnDef
+ :Input
+ :val, :evt
+
+ -- redundant exports, to keep anything an extension might need in one import
+ :ValueStream, :EventStream, :IOStream
+ :Result, :Error
+}
diff --git a/alv/base/input.moon b/alv/base/input.moon
new file mode 100644
index 0000000..19e55a4
--- /dev/null
+++ b/alv/base/input.moon
@@ -0,0 +1,146 @@
+----
+-- Update scheduling policy for `Op` arguments.
+--
+-- @classmod Input
+import ValueStream, EventStream, IOStream from require 'alv.stream'
+import Result from require 'alv.result'
+
+inherits = (klass, frm) ->
+ assert klass, "cant find the ancestor of nil"
+ return true if klass == frm
+ while klass.__parent
+ return true if klass.__parent == frm
+ klass = klass.__parent
+ false
+
+match_parent = (inst, map) ->
+ klass = assert inst and inst.__class, "not an instance"
+ if key = map[klass]
+ return key
+
+ while klass.__parent
+ if key = map[klass.__parent]
+ return key
+
+ klass = klass.__parent
+
+local ColdInput, ValueInput, IOInput, mapping
+
+class Input
+--- Input interface.
+--
+-- Methods that have to be implemented by `Input` implementations.
+-- @section interface
+
+ --- create a new Input.
+ --
+ -- @classmethod
+ -- @tparam Stream stream
+ new: (@stream) =>
+ assert @stream, "nil passed to Input: #{value}"
+
+ --- copy state from old instance (optional).
+ --
+ -- called by `Op:setup` with another `Input` instance or `nil` once this instance is
+ -- registered. Must prepare this instance for `dirty`.
+ --
+ -- May enter a 'setup state' that is exited using `finish_setup`.
+ --
+ -- @tparam ?Input prev previous `Input` intance or nil
+ setup: (prev) =>
+
+ --- whether this input requires processing (optional).
+ --
+ -- must return a boolean indicating whether `Op`s that refer to this instance
+ -- should be notified (via `Op:tick`). If not overwritten, delegates to
+ -- `stream`:@{ValueStream:dirty|dirty}.
+ --
+ -- @treturn bool whether processing is necessary
+ dirty: => @stream\dirty!
+
+ --- leave setup state (optional).
+ --
+ -- called after the `Op` has completed (or skipped) its first `Op:tick` after
+ -- `Op:setup`. Must prepare this instance for dataflow operation.
+ finish_setup: =>
+
+ --- unwrap to Lua value (optional).
+ --
+ -- @treturn any the raw Lua value
+ unwrap: => @stream\unwrap!
+
+ --- return the type name of this `Input` (optional).
+ type: => @stream.type
+
+ --- return the metatype name of this `Input` (optional).
+ metatype: => @stream.metatype
+
+ --- the current value
+ --
+ -- @tfield ValueStream stream
+
+--- members
+-- @section members
+
+ --- alias for `unwrap`.
+ __call: => @stream\unwrap!
+
+ __tostring: => "#{@@__name}:#{@stream}"
+ __inherited: (cls) =>
+ cls.__base.__call = @__call
+ cls.__base.__tostring = @__tostring
+
+--- static functions
+-- @section static
+
+ --- Create a `cold` `Input`.
+ --
+ -- Never marked dirty. Use this for input streams that are only read when
+ -- another `Input` is dirty.
+ --
+ -- @tparam Stream|Result value
+ @cold: (value) ->
+ if value.__class == Result
+ value = assert value.value, "Input from result without value!"
+ ColdInput value
+
+ --- Create a `hot` `Input`.
+ --
+ -- Behaviour depends on what kind of `Stream` `value` is:
+ --
+ -- - `ValueStream`: Marked dirty for the eval-tick if old and new `ValueStream` differ.
+ -- - `EventStream` and `IOStream`: Marked dirty only if the current `EventStream` is dirty.
+ --
+ -- This is the most common `Input` strategy.
+ --
+ -- @tparam Stream|Result value
+ @hot: (value) ->
+ if value.__class == Result
+ value = assert value.value, "Input from result without value!"
+
+ InputType = match_parent value, mapping
+ assert InputType, "Input from unknown value: #{value}"
+ InputType value
+
+class ColdInput extends Input
+ dirty: => false
+
+class ValueInput extends Input
+ setup: (old) => @dirty_setup = not old or @stream != old.stream
+ finish_setup: => @dirty_setup = nil
+ dirty: =>
+ return @dirty_setup if @dirty_setup != nil
+ @stream\dirty!
+
+class IOInput extends Input
+ io: true
+
+mapping = {
+ [ValueStream]: ValueInput
+ [EventStream]: Input
+ [IOStream]: IOInput
+}
+
+{
+ :Input
+}
diff --git a/alv/base/match.moon b/alv/base/match.moon
new file mode 100644
index 0000000..62075c0
--- /dev/null
+++ b/alv/base/match.moon
@@ -0,0 +1,302 @@
+-----
+--- Pattern capturing for Op argument parsing.
+--
+-- There is only one basic buildings block for assembling patterns:
+-- `Type`. It can match `ValueStream`s and `EventStream`s depending on its
+-- metatype argument and can take an optional type name to match as an argument.
+--
+-- In addition to this primitive, the following modifiers are available:
+-- `Repeat`, `Sequence`, `Choice`, and `Optional`. They can be used directly,
+-- but there is also a number of shorthands for assembling patterns quickly:
+--
+-- - `val()` and `evt()`: Shorthands for `Type('value')` and `Type('event')`
+-- - `val.num`: Shorthand for `Type('value', 'num')`
+-- - `evt.str`: Shorthand for `Type('event', 'str')`
+-- - `pat * 2`: Shorthand for `Repeat(pat, 1, 2)` (1-4 times `pat`)
+-- - `pat * 0`: Shorthand for `Repeat(pat, 1, nil)` (1-* times `pat`)
+-- - `pat ^ 2`: Shorthand for `Repeat(pat, 0, 2)` (0-4 times `pat`)
+-- - `pat ^ 0`: Shorthand for `Repeat(pat, 0, nil)` (0-* times `pat`)
+-- - `a + b + … + z`: Shorthand for `Sequence{ a, b, ..., z }`
+-- - `a / b / … / z`: Shorthand for `Choice{ a, b, ..., z }`
+-- - `-pat`: Shorthand for `Optional(pat)`
+--
+-- To perform the actual matching, call the `:match` method on a pattern and
+-- pass a sequence of `Result`s. The method will either return the captured
+-- `Result`s (or a table structuring them)
+--
+-- Any ambiguous pattern can be set to 'recall mode' by invoking it.
+-- Recalling patterns will memorize the first Result they match, and
+-- only match further Results of the same type. For example
+--
+-- arg = (val.num / val.str)!
+-- pattern = arg + arg
+--
+-- ...will match either two numbers or two strings, but not one number and one
+-- string. Recalling works on `Choice` and `Type` patterns. `Type` patterns
+-- without a type (`val!` and `evt!`) always behave like this.
+--
+-- On `Sequence` patterns, a special method `:named` exists. It takes a
+-- sequence of keys that are used instead of integers when constructing the
+-- capture table:
+--
+-- pattern = (val.str + val.num):named('key', 'value')
+-- pattern:match(...)
+-- -- returns { {key='a', value=1}, {key='b', value=2}, ...}
+--
+-- @module base.match
+import Error from require 'alv.error'
+import ValueStream, EventStream from require 'alv.stream'
+
+local Repeat, Sequence, Choice, Optional
+
+typestr = (result) ->
+ str = result\type!
+ str ..= '!' if result\metatype! == 'event'
+ str
+
+class Pattern
+ match: (seq) =>
+ @reset!
+ num, cap = @capture seq, 1
+ if num != #seq
+ args = table.concat [typestr arg for arg in *seq], ' '
+ msg = "couldn't match arguments (#{args}) against pattern #{@}"
+ error Error 'argument', msg
+ cap
+
+ remember: (key) =>
+ return true unless @recall
+
+ @recalled or= key
+ @recalled == key
+
+ rep: (min, max) => Repeat @, min, max
+
+ reset: => @recalled = nil
+
+ __call: => @
+ __mul: (num) => Repeat @, 1, if num != 0 then num
+ __pow: (num) => Repeat @, 0, if num != 0 then num
+ __add: (other) => Sequence { @, other }
+ __div: (other) => Choice { @, other }
+ __unm: => Optional @
+
+ __inherited: (cls) =>
+ cls.__base.__call or= @__call
+ cls.__base.__mul or= @__mul
+ cls.__base.__pow or= @__pow
+ cls.__base.__add or= @__add
+ cls.__base.__div or= @__div
+ cls.__base.__unm or= @__unm
+
+--- Base Stream Pattern.
+--
+-- When instantiated with `type`, only succeeds for `Stream`s whose value and
+-- meta types match.
+--
+-- Otherwise, matches Streams based only on `metatype` for the first match, but
+-- using both afterwards (recall mode).
+--
+-- @function Stream
+-- @tparam string metatype "value" or "event"
+-- @tparam ?string type type name
+class Type extends Pattern
+ new: (@metatype, @type) =>
+ @recall = not @type
+
+ capture: (seq, i) =>
+ return unless seq[i]
+ type, mt = seq[i]\type!, seq[i]\metatype!
+ return unless @metatype == mt
+ match = if @type then type == @type else @remember type
+ if match
+ 1, seq[i]
+
+ __tostring: =>
+ str = @type or @metatype
+ str ..= '!' if @metatype == 'event'
+ str
+
+--- Repeat a pattern.
+--
+-- Matches a given `inner` pattern as many times as possible, within the given
+-- minimum/maximum counts. Matching this pattern results in a sequence of the
+-- individual captures produced by the inner pattern.
+--
+-- @function Repeat
+-- @tparam Pattern inner the original pattern
+-- @tparam ?number min minimum amount of repetitions
+-- @tparam ?number max maximum amount of repetitions (default infinite)
+class Repeat extends Pattern
+ new: (@inner, @min, @max) =>
+
+ capture: (seq, i) =>
+ take, all = 0, {}
+ while true
+ num, cap = @inner\capture seq, i+take
+ break unless num
+
+ take += num
+ table.insert all, cap
+
+ break if @max and take >= @max
+
+ return if @min and take < @min
+ return if @max and take > @max
+
+ take, all
+
+ reset: =>
+ @inner\reset!
+
+ __call: =>
+ @@ @inner!, @min, @max
+
+ __tostring: =>
+ min = @min or '0'
+ max = @max or '*'
+ "#{@inner}{#{min}-#{max}}"
+
+--- Match multiple patterns in order.
+--
+-- Matches the inner patterns in order, only succeeds if all of them match.
+-- Captures the individual captures produced by the inner patterns in a
+-- sequence, or table with keys specified in `keys` or using the `:named(...)`
+-- modifier.
+--
+-- @function Sequence
+-- @tparam {Pattern,...} elements the inner patterns
+-- @tparam ?{string,...} keys the keys to use when capturing matches
+class Sequence extends Pattern
+ new: (@elements, @keys) =>
+
+ capture: (seq, i) =>
+ take, all = 0, {}
+ for key, elem in ipairs @elements
+ num, cap = elem\capture seq, i+take
+ return unless num
+
+ take += num
+ key = @keys[key] if @keys
+ all[key] = cap
+
+ take, all
+
+ reset: =>
+ for elem in *@elements
+ elem\reset!
+
+ named: (...) =>
+ @@ [e for e in *@elements], { ... }
+
+ __call: =>
+ @@ [e! for e in *@elements]
+
+ __add: (other) =>
+ elements = [e for e in *@elements]
+ table.insert elements, other
+ @@ elements
+
+ __tostring: =>
+ core = table.concat [tostring e for e in *@elements], ' '
+ "(#{core})"
+
+--- Match one of multiple options.
+--
+-- Matches using the first matching pattern in `elements` and returns its
+-- captured value. Supports recalling the matched subpattern.
+--
+-- @function Choice
+-- @tparam {Pattern,...} elements the inner patterns
+-- @tparam ?{string,...} keys the keys to use when capturing matches
+class Choice extends Pattern
+ new: (@elements, @recall=false) =>
+
+ capture: (seq, i) =>
+ for key, elem in ipairs @elements
+ num, cap = elem\capture seq, i
+ if num and @remember key
+ return num, cap
+
+ reset: =>
+ super!
+ for elem in *@elements
+ elem\reset!
+
+ __call: =>
+ @@ [e! for e in *@elements], true
+
+ __div: (other) =>
+ elements = [e for e in *@elements]
+ table.insert elements, other
+ @@ elements
+
+ __tostring: =>
+ core = table.concat [tostring e for e in *@elements], ' | '
+ "(#{core})"
+
+--- Optionally match a pattern.
+--
+-- Matches using the first matching pattern in `elements` and returns its
+-- captured value. Supports recalling the matched subpattern.
+--
+-- @function Optional
+-- @tparam {Pattern,...} elements the inner patterns
+-- @tparam ?{string,...} keys the keys to use when capturing matches
+class Optional extends Pattern
+ new: (@inner) =>
+
+ capture: (seq, i) =>
+ num, cap = @inner\capture seq, i
+ num or 0, cap
+
+ reset: =>
+ @inner\reset!
+
+ __call: =>
+ @@ @inner!
+
+ __unm: => @
+
+ __tostring: => "#{@inner}?"
+
+--- `Value` shorthands.
+--
+-- Call or index with a string to obtain a `Type` instance.
+-- Call to obtain a wildcard pattern.
+--
+-- val.str, val.num
+-- val['vec3'], val('vec3')
+-- val()
+--
+-- @table val
+val = setmetatable {}, {
+ __index: (key) =>
+ with v = Type 'value', key
+ @[key] = v
+
+ __call: (...) => Type 'value', ...
+}
+
+--- `Event` shorthands.
+--
+-- Call or index with a string to obtain an `Type` instance.
+-- Call to obtain a wildcard pattern.
+--
+-- evt.bang, evt.str, evt.num
+-- evt['midi/message'], evt('midi/message')
+-- evt()
+--
+-- @table evt
+evt = setmetatable {}, {
+ __index: (key) =>
+ with v = Type 'event', key
+ @[key] = v
+
+ __call: (...) => Type 'event', ...
+}
+
+{
+ :Type, :Repeat, :Sequence, :Choice, :Optional
+ :val, :evt
+}
diff --git a/alv/base/op.moon b/alv/base/op.moon
new file mode 100644
index 0000000..b0a83dc
--- /dev/null
+++ b/alv/base/op.moon
@@ -0,0 +1,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
+}