aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authors-ol <s-ol@users.noreply.github.com>2020-03-18 17:50:33 +0000
committers-ol <s-ol@users.noreply.github.com>2020-03-18 18:03:04 +0000
commita49382c7b46c7bdf1c105c0783c7bdcd0bb70e4a (patch)
treec1f800736e1905514aa913630ee31a720e770c9e
parentdocs/internal: add builtins and invoke modules (diff)
downloadalive-a49382c7b46c7bdf1c105c0783c7bdcd0bb70e4a.tar.gz
alive-a49382c7b46c7bdf1c105c0783c7bdcd0bb70e4a.zip
fork ops before eval cycle
-rw-r--r--core/base/op.moon12
-rw-r--r--core/invoke.moon4
-rw-r--r--core/value.moon9
-rw-r--r--lib/midi/core.moon2
-rw-r--r--lib/midi/launchctl.moon44
-rw-r--r--lib/random.moon10
-rw-r--r--lib/time.moon43
-rw-r--r--lib/util.moon33
-rw-r--r--spec/core/value_spec.moon120
9 files changed, 164 insertions, 113 deletions
diff --git a/core/base/op.moon b/core/base/op.moon
index f338673..08c47ab 100644
--- a/core/base/op.moon
+++ b/core/base/op.moon
@@ -19,6 +19,17 @@ class Op
-- @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 Value
+ fork: =>
+ with setmetatable {}, getmetatable @
+ .state = {k,v for k,v in pairs @state} if @state
+ .out = @out\fork! if @out
+
--- `Value` instance representing this Op's computed output value.
--
-- Must be set to a `Value` instance once `setup` finishes. Must not change
@@ -92,6 +103,7 @@ class Op
-- @tparam[opt] string type the type-name for `out`
-- @tparam[optchain] any init the initial value for `out`
new: (type, init) =>
+ @state = {}
if type
@out = Value type, init
diff --git a/core/invoke.moon b/core/invoke.moon
index 8d63962..23207b7 100644
--- a/core/invoke.moon
+++ b/core/invoke.moon
@@ -13,7 +13,9 @@ import Scope from require 'core.scope'
class op_invoke extends Action
--- `Action:patch` implementation.
patch: (head) =>
- return true if head == @head
+ if head == @head
+ @op = @op\fork!
+ return true
@op\destroy! if @op
diff --git a/core/value.moon b/core/value.moon
index 0f40ef3..bda6127 100644
--- a/core/value.moon
+++ b/core/value.moon
@@ -36,6 +36,15 @@ class Value
assert type == @type, msg or "#{@} is not a #{type}" if type
@value
+ --- create a mutable copy of this Value.
+ --
+ -- Used to wrap insulate eval-cycles from each other.
+ --
+ -- @treturn Value
+ fork: =>
+ with Value @type, @value, @raw
+ .updated = @updated
+
--- alias for `unwrap`.
__call: (...) => @unwrap ...
diff --git a/lib/midi/core.moon b/lib/midi/core.moon
index d5259ad..8378b1d 100644
--- a/lib/midi/core.moon
+++ b/lib/midi/core.moon
@@ -101,7 +101,7 @@ apply_range = (range, val) ->
when 'rad' then val / 64 * math.pi
when 'deg' then val / 128 * 360
else
- error "unknown range #{@range}"
+ error "unknown range #{range}"
elseif range.type == 'num'
val / 128 * range\unwrap!
else
diff --git a/lib/midi/launchctl.moon b/lib/midi/launchctl.moon
index 6408528..9f29922 100644
--- a/lib/midi/launchctl.moon
+++ b/lib/midi/launchctl.moon
@@ -19,9 +19,7 @@ range can be one of:
- 'deg' [ 0 - 360[
- (num) [ 0 - num["
- new: =>
- super 'num'
- @steps = {}
+ new: => super 'num'
setup: (inputs) =>
{ port, i, start,
@@ -42,23 +40,23 @@ range can be one of:
{ :port, :i, :start, :chan, :steps, :range } = @inputs
if steps\dirty!
- while steps! > #@steps
- table.insert @steps, 0
- while steps! < #@steps
- table.remove @steps
+ while steps! > #@state
+ table.insert @state, 0
+ while steps! < #@state
+ table.remove @state
- curr_i = i! % #@steps
+ curr_i = i! % #@state
if port\dirty!
changed = false
for msg in port!\receive!
if msg.status == 'control-change' and msg.chan == chan!
rel_i = msg.a - start!
- if rel_i >= 0 and rel_i < #@steps
- @steps[rel_i+1] = msg.b
+ if rel_i >= 0 and rel_i < #@state
+ @state[rel_i+1] = msg.b
changed = rel_i == curr_i
- @out\set apply_range range, @steps[curr_i+1] if changed
+ @out\set apply_range range, @state[curr_i+1] if changed
else
- @out\set apply_range range, @steps[curr_i+1]
+ @out\set apply_range range, @state[curr_i+1]
class gate_seq extends Op
@doc: "(launctl/gate-seq port i start chan [steps]) - Gate-Sequencer
@@ -68,7 +66,7 @@ steps defaults to 8."
new: =>
super 'bool', false
- @steps = {}
+ @state = {}
setup: (inputs) =>
{ port, i, start, chan, steps } = match 'midi/port num num num num?', inputs
@@ -91,34 +89,34 @@ steps defaults to 8."
display: (i, active) =>
{ :port, :start, :chan } = @unwrap_all!
- port\send 'note-on', chan, (start + i), light @steps[i+1], active
+ port\send 'note-on', chan, (start + i), light @state[i+1], active
tick: =>
{ :port, :i, :start, :chan, :steps } = @inputs
if steps\dirty!
- while steps! > #@steps
- table.insert @steps, false
- while steps! < #@steps
- table.remove @steps
+ while steps! > #@state
+ table.insert @state, false
+ while steps! < #@state
+ table.remove @state
- curr_i = i! % #@steps
+ curr_i = i! % #@state
if port\dirty!
for msg in port!\receive!
if msg.status == 'note-on' and msg.chan == chan!
rel_i = msg.a - start!
- if rel_i >= 0 and rel_i < #@steps
- @steps[rel_i+1] = not @steps[rel_i+1]
+ if rel_i >= 0 and rel_i < #@state
+ @state[rel_i+1] = not @state[rel_i+1]
@display rel_i, rel_i == curr_i
if i\dirty!
- prev_i = (curr_i - 1) % #@steps
+ prev_i = (curr_i - 1) % #@state
@display curr_i, true
@display prev_i, false
- @out\set @steps[curr_i+1]
+ @out\set @state[curr_i+1]
{
'gate-seq': gate_seq
diff --git a/lib/random.moon b/lib/random.moon
index 7cabdf0..b97671d 100644
--- a/lib/random.moon
+++ b/lib/random.moon
@@ -8,7 +8,7 @@ apply_range = (range, val) ->
when 'rad' then val * 2 * math.pi
when 'deg' then val * 360
else
- error "unknown range #{@range}"
+ error "unknown range #{range}"
elseif range.type == 'num'
val * range\unwrap!
else
@@ -31,7 +31,7 @@ generates a random value in range on create and trigger.
super 'num'
@gen!
- gen: => @val = math.random!
+ gen: => @state = { math.random! }
setup: (inputs) =>
{ trig, range } = match 'bang? any?', inputs
@@ -41,7 +41,7 @@ generates a random value in range on create and trigger.
tick: =>
@gen! if @inputs.trig and @inputs.trig\dirty!
- @out\set apply_range @inputs.range, @val
+ @out\set apply_range @inputs.range, @state[1]
vec_ = (n) ->
class vec extends Op
@@ -54,7 +54,7 @@ each component is in range.
super "vec#{n}"
@gen!
- gen: => @val = for i=1,n do math.random!
+ gen: => @state = for i=1,n do math.random!
setup: (inputs) =>
{ trig, range } = match 'bang? any?', inputs
@@ -64,7 +64,7 @@ each component is in range.
tick: =>
@gen! if @inputs.trig and @inputs.trig\dirty!
- @out\set [apply_range @inputs.range, v for v in *@val]
+ @out\set [apply_range @inputs.range, v for v in *@state]
vec.__name = "vec#{n}"
vec
diff --git a/lib/time.moon b/lib/time.moon
index 6e70355..8e9aded 100644
--- a/lib/time.moon
+++ b/lib/time.moon
@@ -47,7 +47,7 @@ wave selects the wave shape from the following:
new: =>
super 'num'
- @phase = 0
+ @state.phase or= 0
default_wave = Value.str 'sin'
setup: (inputs, scope) =>
@@ -62,11 +62,11 @@ wave selects the wave shape from the following:
if @inputs.clock\dirty!
{ :clock, :freq, :wave } = @unwrap_all!
- @phase += clock.dt * freq
+ @state.phase += clock.dt * freq
@out\set switch wave
- when 'sin' then .5 + .5 * math.cos @phase * tau
- when 'saw' then @phase % 1
- when 'tri' then math.abs (2*@phase % 2) - 1
+ when 'sin' then .5 + .5 * math.cos @state.phase * tau
+ when 'saw' then @state.phase % 1
+ when 'tri' then math.abs (2*@state.phase % 2) - 1
else error "unknown wave type"
class ramp extends Op
@@ -76,7 +76,7 @@ ramps from 0 to max (default same as ramp) once every period seconds."
new: =>
super 'num'
- @phase = 0
+ @state.phase or= 0
setup: (inputs, scope) =>
{ clock, period, max } = match 'clock? num num?', inputs
@@ -90,21 +90,22 @@ ramps from 0 to max (default same as ramp) once every period seconds."
if clock_dirty
{ :clock, :period, :max } = @unwrap_all!
max or= period
- @phase += clock.dt / period
+ @state.phase += clock.dt / period
- while @phase >= 1
- @phase -= 1
+ while @state.phase >= 1
+ @state.phase -= 1
if clock_dirty or (@inputs.max and @inputs.max\dirty!)
- @out\set @phase * max
+ @out\set @state.phase * max
class tick extends Op
@doc: "(tick [clock] period) - count ticks
counts upwards by one every period seconds and returns the number of completed ticks."
new: =>
- @phase, @count = 0, 0
- super 'num', @count
+ super 'num', 0
+ @state.phase or= 0
+ @state.count or= 0
setup: (inputs, scope) =>
{ clock, period } = match 'clock? num', inputs
@@ -115,12 +116,12 @@ counts upwards by one every period seconds and returns the number of completed t
tick: =>
if @inputs.clock\dirty!
{ :clock, :period, :max } = @unwrap_all!
- @phase += clock.dt / period
+ @state.phase += clock.dt / period
- while @phase >= 1
- @phase -= 1
- @count += 1
- @out\set @count
+ while @state.phase >= 1
+ @state.phase -= 1
+ @state.count += 1
+ @out\set @state.count
class every extends Op
@doc: "(every [clock] period) - trigger every period seconds
@@ -128,7 +129,7 @@ class every extends Op
returns true once every period seconds."
new: =>
super 'bang'
- @phase = 0
+ @state.phase or= 0
setup: (inputs, scope) =>
{ clock, period } = match 'clock? num', inputs
@@ -139,10 +140,10 @@ returns true once every period seconds."
tick: =>
if @inputs.clock\dirty!
{ :clock, :period, :max } = @unwrap_all!
- @phase += clock.dt / period
+ @state.phase += clock.dt / period
- while @phase >= 1
- @phase -= 1
+ while @state.phase >= 1
+ @state.phase -= 1
@out\set true
{
diff --git a/lib/util.moon b/lib/util.moon
index 79ee721..1fddca4 100644
--- a/lib/util.moon
+++ b/lib/util.moon
@@ -71,35 +71,6 @@ when i is a num, it is (floor)ed and the matching argument (starting from 0) is
if active and active\dirty!
@out\set active!
---class switch_pause extends Op
--- @doc: "(switch- i v0 [v1 v2...]) - switch and pause multiple inputs
---
---like (switch ...) except that the unused inputs are paused."
---
--- setup: (@i, ...) =>
--- @choices = { ... }
---
--- typ = @choices[1].type
--- for inp in *@choices[2,]
--- assert inp.type == typ, "not all values have the same type: #{typ} != #{inp.type}"
---
--- @out = Stream typ
--- @out
---
--- tick: =>
--- i = @i\unwrap!
--- active = switch i
--- when true
--- @choices[1]
--- when false
--- @choices[2]
--- else
--- i = 1 + (math.floor i) % #@choices
--- @choices[i]
---
--- @out\set if active
--- active\unwrap!
-
class edge extends Op
@doc: "(edge bool) - convert rising edges to bangs"
new: => super 'bang'
@@ -110,9 +81,9 @@ class edge extends Op
tick: =>
now = @inputs.value!
- if now and not @last
+ if now and not @state.last
@out\set true
- @last = now
+ @state.last = now
class default extends Op
@doc: "(default stream default) - provide a default value for an event stream
diff --git a/spec/core/value_spec.moon b/spec/core/value_spec.moon
index 2436371..eeb3339 100644
--- a/spec/core/value_spec.moon
+++ b/spec/core/value_spec.moon
@@ -1,4 +1,4 @@
-import Value, Result, Scope from require 'core'
+import Value, Result, Scope, SimpleRegistry from require 'core'
import Op, Action from require 'core.base'
import Logger from require 'logger'
Logger.init 'silent'
@@ -9,57 +9,61 @@ class TestOp extends Op
class TestAction extends Action
new: (...) =>
+reg = SimpleRegistry!
+setup -> reg\grab!
+teardown -> reg\release!
+
describe 'Value', ->
- describe 'wraps', ->
- test 'numbers', ->
+ describe '.wrap', ->
+ it 'wraps numbers', ->
got = Value.wrap 3
assert.is.equal 'num', got.type
assert.is.equal 3, got.value
- test 'strings', ->
+ it 'wraps strings', ->
got = Value.wrap "im a happy string"
assert.is.equal 'str', got.type
assert.is.equal "im a happy string", got.value
- test 'Values', ->
+ it 'wraps Values', ->
pi = Value 'num', 3.14
got = Value.wrap pi
assert.is.equal pi, got
- test 'Opdefs', ->
+ it 'wraps Opdefs', ->
got = Value.wrap TestOp
assert.is.equal 'opdef', got.type
assert.is.equal TestOp, got.value
- test 'Bultins', ->
+ it 'wraps Bultins', ->
got = Value.wrap TestAction
assert.is.equal 'builtin', got.type
assert.is.equal TestAction, got.value
- test 'Scopes', ->
+ it 'wraps Scopes', ->
sub = Scope!
got = Value.wrap sub
assert.is.equal 'scope', got.type
assert.is.equal sub, got.value
- test 'tables', ->
+ it 'wraps tables', ->
pi = Value 'num', 3.14
got = Value.wrap { :pi }
assert.is.equal 'scope', got.type
assert.is.equal pi, (got.value\get 'pi')\const!
- describe 'unwraps', ->
- test 'unwrap!', ->
+ describe ':unwrap', ->
+ it 'returns the raw value!', ->
assert.is.equal 3.14, (Value.num 3.14)\unwrap!
assert.is.equal 'hi', (Value.str 'hi')\unwrap!
assert.is.equal 'hi', (Value.sym 'hi')\unwrap!
- test 'with type assert', ->
+ test 'can assert the type', ->
assert.is.equal 3.14, (Value.num 3.14)\unwrap 'num'
assert.is.equal 'hi', (Value.str 'hi')\unwrap 'str'
assert.is.equal 'hi', (Value.sym 'hi')\unwrap 'sym'
@@ -67,7 +71,7 @@ describe 'Value', ->
assert.has_error -> (Value.str 'hi')\unwrap 'num'
assert.has_error -> (Value.sym 'hi')\unwrap 'str'
- test 'with __call shorthand', ->
+ test 'has __call shorthand', ->
assert.is.equal 3.14, (Value.num 3.14)!
assert.is.equal 'hi', (Value.str 'hi')!
assert.is.equal 'hi', (Value.sym 'hi')!
@@ -78,8 +82,8 @@ describe 'Value', ->
assert.has_error -> (Value.str 'hi') 'num'
assert.has_error -> (Value.sym 'hi') 'str'
- describe 'checks equality', ->
- test 'using the type', ->
+ describe 'overrides __eq', ->
+ it 'compares the type', ->
val = Value 'num', 3
assert.is.equal (Value.num 3), val
assert.not.equal (Value.str '3'), val
@@ -88,20 +92,36 @@ describe 'Value', ->
assert.is.equal (Value.str 'hello'), val
assert.not.equal (Value.sym 'hello'), val
- test 'using the value', ->
+ it 'compares the value', ->
val = Value 'num', 3
assert.is.equal (Value.num 3), val
assert.not.equal (Value.num 4), val
- describe 'evaluates literal', ->
- test 'numbers to consts', ->
+ describe ':set', ->
+ it 'sets the value', ->
+ val = Value 'num', 3
+ assert.is.equal (Value.num 3), val
+
+ val\set 4
+ assert.is.equal (Value.num 4), val
+ assert.not.equal (Value.num 3), val
+
+ it 'marks the value dirty', ->
+ val = Value 'num', 3
+ assert.is.false val\dirty!
+
+ val\set 4
+ assert.is.true val\dirty!
+
+ describe ':eval', ->
+ it 'turns numbers into consts', ->
assert_noop = (val) ->
assert.is.equal val, val\eval!\const!
assert_noop Value.num 2
assert_noop Value.str 'hello'
- test 'symbols in the scope', ->
+ it 'looks up symbols in the scope', ->
scope = with Scope!
\set 'number', Result value: Value.num 3
\set 'hello', Result value: Value.str "world"
@@ -115,18 +135,56 @@ describe 'Value', ->
assert_eval 'hello', Value.str "world"
assert_eval 'goodbye', Value.sym "again"
- describe 'quotes literals', ->
- test 'as themselves', ->
- assert_noop = (val) -> assert.is.equal val, val\quote!
+ it ':quote s literals as themselves', ->
+ assert_noop = (val) -> assert.is.equal val, val\quote!
- assert_noop Value.num 2
- assert_noop Value.str 'hello'
- assert_noop Value.sym 'world'
+ assert_noop Value.num 2
+ assert_noop Value.str 'hello'
+ assert_noop Value.sym 'world'
- describe 'clones literals', ->
- test 'as themselves', ->
- assert_noop = (val) -> assert.is.equal val, val\clone!
+ it ':clone sliterals as themselves', ->
+ assert_noop = (val) -> assert.is.equal val, val\clone!
- assert_noop Value.num 2
- assert_noop Value.str 'hello'
- assert_noop Value.sym 'world'
+ assert_noop Value.num 2
+ assert_noop Value.str 'hello'
+ assert_noop Value.sym 'world'
+
+ describe ':fork', ->
+ it 'is equal to the original', ->
+ a = Value.num 2
+ b = Value.str 'asdf'
+ c = with Value 'weird', {}, '(raw)'
+ \set {}
+
+ aa, bb, cc = a\fork!, b\fork!, c\fork!
+ assert.is.equal a, aa
+ assert.is.equal b, bb
+ assert.is.equal c, cc
+
+ assert.is.false aa\dirty!
+ assert.is.false bb\dirty!
+ assert.is.true cc\dirty!
+
+ assert.is.equal c.raw, cc.raw
+
+ it 'isolates the original from the fork', ->
+ a = Value.num 3
+ b = with Value 'weird', {}, '(raw)'
+ \set {}
+
+ aa, bb = a\fork!, b\fork!
+
+ bb\set {false}
+
+ assert.is.same {}, b!
+ assert.is.same {false}, bb!
+ assert.is.true b\dirty!
+ assert.is.true bb\dirty!
+
+ reg\next_tick!
+ aa\set 4
+
+ assert.is.equal 3, a!
+ assert.is.equal 4, aa!
+ assert.is.false a\dirty!
+ assert.is.true aa\dirty!