aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authors-ol <s-ol@users.noreply.github.com>2020-03-01 17:44:56 +0000
committers-ol <s-ol@users.noreply.github.com>2020-03-01 17:46:04 +0000
commita1312100fad100190e20b1b3443b5cd962dc8999 (patch)
tree8fc6194fc4b326ee39a9ca6d798cd208664f5fc3
parentadd type-pattern matching language (diff)
downloadalive-a1312100fad100190e20b1b3443b5cd962dc8999.tar.gz
alive-a1312100fad100190e20b1b3443b5cd962dc8999.zip
new op interface part one
-rw-r--r--core/base.moon77
-rw-r--r--core/cell.moon4
-rw-r--r--core/invoke.moon8
-rw-r--r--core/pattern.moon4
-rw-r--r--core/registry.moon6
-rw-r--r--core/value.moon33
-rw-r--r--lib/debug.moon16
-rw-r--r--lib/midi/core.moon66
-rw-r--r--lib/midi/init.moon50
-rw-r--r--lib/time.moon119
-rw-r--r--spec/core/pattern_spec.moon26
11 files changed, 239 insertions, 170 deletions
diff --git a/core/base.moon b/core/base.moon
index 422e631..0a88cc1 100644
--- a/core/base.moon
+++ b/core/base.moon
@@ -1,8 +1,40 @@
-- base definitions for extensions
-import Value from require 'core.value'
+import Value, Result from require 'core.value'
unpack or= table.unpack
+class Input
+ new: (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}"
+
+ merge: (previous) =>
+
+ finish_setup: =>
+ dirty: => @stream\dirty!
+ unwrap: => @stream\unwrap!
+ type: => @stream.type
+ __call: => @stream\unwrap!
+ __inherited: (cls) => cls.__base.__call = @__call
+
+-- ValueInput scheduling policy
+--
+-- during setup, only marked dirty if old and new stream differ in value
+class ValueInput extends Input
+ merge: (old) => @dirty_setup = not old or @stream\unwrap! != old\unwrap!
+ finish_setup: => @dirty_setup = false
+ dirty: => @dirty_setup or @stream\dirty!
+
+-- EventInput scheduling policy
+--
+-- only marked dirty if the input stream itself is dirty
+class EventInput extends Input
+
-- a persistent expression Operator
--
-- accepts Const or Stream inputs and produces a Stream output
@@ -17,7 +49,37 @@ class Op
-- after this method finishes, :tick(true) is called once, after which
-- @impulses and @out have to be set and may not change until :setup()
-- is called again.
- setup: (@inputs) =>
+ setup: do
+ do_merge = (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_merge old_val, cur_val
+ elseif cur_plain == old_plain
+ -- both are streams (or nil), merge them
+ cur_val\merge old_val
+
+ (inputs) =>
+ old_inputs = @inputs
+ @inputs = inputs
+ do_merge old_inputs, @inputs
+
+ -- 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
-- called once per frame if any inputs or impulses are dirty, and once
-- immediately after :setup(). 'first' will be true in the latter case.
@@ -28,8 +90,14 @@ class Op
destroy: =>
-- utilities
- unwrap_inputs: =>
- unpack [input! for input in *@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 '#', ...
@@ -123,6 +191,7 @@ class FnDef
"(fn (#{table.concat [p\stringify! for p in *@params], ' '}) ...)"
{
+ :ValueInput, :EventInput
:Dispatcher
:Op
:Action
diff --git a/core/cell.moon b/core/cell.moon
index 4b28a04..c052d46 100644
--- a/core/cell.moon
+++ b/core/cell.moon
@@ -19,10 +19,6 @@ class Cell
when 'builtin'
head\unwrap!
else
- print head
- for k,v in pairs head
- print k,v
- print head.__class.__name
error "cannot evaluate expr with head #{head}"
Action\eval_cell scope, @tag, head, @tail!
diff --git a/core/invoke.moon b/core/invoke.moon
index da82d00..7b14e97 100644
--- a/core/invoke.moon
+++ b/core/invoke.moon
@@ -10,15 +10,15 @@ class op_invoke extends Action
def = head\unwrap 'opdef', "cant op-invoke #{@head}"
@head, @op = head, def!
- @first = true
true
eval: (scope, tail) =>
children = L\push -> [L\push expr\eval, scope for expr in *tail]
- @op\setup [child.value for child in *children]
- @op\tick @first
- @first = nil
+ @op\setup [result for result in *children]
+ @op\tick true
+ for input in @op\all_inputs!
+ input\finish_setup!
Result :children, value: @op.out, op: @op
diff --git a/core/pattern.moon b/core/pattern.moon
index 45701e6..64b53b8 100644
--- a/core/pattern.moon
+++ b/core/pattern.moon
@@ -40,7 +40,7 @@ class Pattern
else
matches = @matches results[1]
assert @opt or matches, "couldn't match argument #{results[1]} as type #{@type}!"
- matches and table.remove results, 1
+ if matches then table.remove results, 1
match = (pattern, results) ->
patterns = while pattern
@@ -50,7 +50,7 @@ match = (pattern, results) ->
Pattern pat
values = [p\match results for p in *patterns]
assert #results == 0, "#{#results} extra arguments given!"
- unpack values
+ values
{
:Pattern
diff --git a/core/registry.moon b/core/registry.moon
index 2ac5825..2a83f7e 100644
--- a/core/registry.moon
+++ b/core/registry.moon
@@ -1,11 +1,11 @@
-import Value from require 'core.value'
+import Result, Value from require 'core.value'
class Registry
new: () =>
@map = {}
@tick = 0
- @kr = Value.bool true
+ @kr = Result value: Value.bool true
-- methods for Tag
@@ -48,7 +48,7 @@ class Registry
wrap_tick: (fn) => (...) ->
@grab!
@tick += 1
- @kr\set true
+ @kr.value\set true
with fn ...
@release!
diff --git a/core/value.moon b/core/value.moon
index 5379921..0d44320 100644
--- a/core/value.moon
+++ b/core/value.moon
@@ -27,36 +27,38 @@ class Result
@op = params.op
@children = params.children or {}
- @all_impulses = {}
+ @side_inputs, is_child = {}, {}
for child in *@children
- for d in pairs child.all_impulses
- @all_impulses[d] = true
+ for d in pairs child.side_inputs
+ @side_inputs[d] = true
+ if child.value
+ is_child[child.value] = true
if @op
- assert @op.impulses, "#{@op} not set up correctly (impulses)"
- for d in *@op.impulses
- @all_impulses[d] = true
+ for input in @op\all_inputs!
+ continue if is_child[input]
+ @side_inputs[input] = true
- is_const: => not next @all_impulses
+ is_const: => not next @side_inputs
-- asserts value-constness and returns the value
const: (msg) =>
- assert not (next @all_impulses), msg or "eval-time const expected"
+ assert not (next @side_inputs), msg or "eval-time const expected"
@value
-- create a value-copy of this result that has the same impulses but without
-- affecting the original's update logic
make_ref: =>
with Result value: @value
- .all_impulses = @all_impulses
+ .side_inputs = @side_inputs
-- in depth-first order, tick all Ops who have dirty Stream inputs or impulses
--
-- short-circuits if there are no dirty Streams in the entire subtree
tick: =>
any_dirty = false
- for stream in pairs @all_impulses
- if stream\dirty!
+ for input in pairs @side_inputs
+ if input\dirty!
any_dirty = true
break
@@ -70,16 +72,12 @@ class Result
-- we have to check self_dirty here, because streams from child
-- expressions might have changed
self_dirty = false
- for stream in *@op.impulses
- if stream\dirty!
- self_dirty = true
- break
- for stream in *@op.inputs
+ for stream in @op\all_inputs!
if stream\dirty!
self_dirty = true
break
- L\trace "#{op} is #{if self_dirty then 'dirty' else 'clean'}"
+ L\trace "#{@op} is #{if self_dirty then 'dirty' else 'clean'}"
return unless self_dirty
@op\tick!
@@ -100,7 +98,6 @@ class Value
-- @value - Lua value - access through :unwrap()
new: (@type, @value, @raw) =>
@updated = 0
- pcall @\set, @value
dirty: => @updated == Registry.active!.tick
diff --git a/lib/debug.moon b/lib/debug.moon
index 08a7f78..1457cee 100644
--- a/lib/debug.moon
+++ b/lib/debug.moon
@@ -1,15 +1,19 @@
import Op from require 'core'
+import ValueInput, EventInput from require 'core.base'
+import match from require 'core.pattern'
class out extends Op
- @doc: "(out name-str value) - log value to the console"
+ @doc: "(out [name-str?] value) - log value to the console"
- setup: (params) =>
- super params
- assert @inputs[2], "need a value"
- @assert_types 'str', @inputs[2].type
+ setup: (inputs) =>
+ { name, value } = match 'str? any', inputs
+ super
+ name: name and ValueInput name
+ value: ValueInput value
tick: =>
- L\print @unwrap_inputs!
+ { :name, :value } = @unwrap_all!
+ L\print if name then name, value else value
{
:out
diff --git a/lib/midi/core.moon b/lib/midi/core.moon
index 7f661ed..3bf47a9 100644
--- a/lib/midi/core.moon
+++ b/lib/midi/core.moon
@@ -1,6 +1,8 @@
import RtMidiIn, RtMidiOut, RtMidi from require 'luartmidi'
import band, bor, lshift, rshift from require 'bit32'
import Op, Registry from require 'core'
+import ValueInput, EventInput from require 'core.base'
+import match from require 'core.pattern'
MIDI = {
[0x9]: 'note-on'
@@ -62,58 +64,58 @@ class MidiPort
class input extends Op
@doc: "(midi/input name) - create a MIDI input port"
- new: =>
- super 'midi/port'
- @impulses = { Registry.active!.kr }
+ new: => super 'midi/port'
- setup: (params) =>
- super params
- @assert_types 'str'
+ setup: (inputs) =>
+ { name } = match 'str', inputs
+ super
+ name: ValueInput name
+ root: EventInput Registry.active!.kr
- tick: (first) =>
- { name } = @inputs
- if first or name\dirty!
- @out\set MidiPort find_port RtMidiIn, name\unwrap!
+ tick: =>
+ if @inputs.name\dirty!
+ @out\set MidiPort find_port RtMidiIn, @inputs.name!
@out\unwrap!\tick!
class output extends Op
@doc: "(midi/output name) - create a MIDI output port"
- new: =>
- super 'midi/port'
+ new: => super 'midi/port'
+
+ setup: (inputs) =>
+ { name } = match 'str', inputs
+ super
+ name: ValueInput name
+ root: EventInput Registry.active!.kr
- setup: (params) =>
- super params
- @assert_types 'str'
+ tick: =>
+ if @inputs.name\dirty!
+ @out\set MidiPort nil, find_port RtMidiOut, @inputs.name!
- tick: (first) =>
- { name } = @inputs
- if first or name\dirty!
- @out\set MidiPort nil, find_port RtMidiOut, name\unwrap!
+ @out\unwrap!\tick!
class inout extends Op
@doc: "(midi/inout inname outname) - create a bidirectional MIDI port"
- new: =>
- super 'midi/port'
- @impulses = { Registry.active!.kr }
-
- setup: (params) =>
- super params
- @assert_types 'str', 'str'
+ new: => super 'midi/port'
- tick: (first) =>
- { inp, out } = @inputs
+ setup: (inputs) =>
+ { inp, out } = match 'str, str', inputs
+ super
+ inp: ValueInput inp
+ out: ValueInput out
+ root: EventInput Registry.active!.kr
- if first or inp\dirty! or out\dirty!
- inp, out = inp\unwrap!, out\unwrap!
- @out\set MidiPort (find_port RtMidiIn, inp), (find_port RtMidiOut, out)
+ tick: =>
+ { :inp, :out } = @inputs
+ if inp\dirty! or out\dirty!
+ @out\set MidiPort (find_port RtMidiIn, inp!), (find_port RtMidiOut, out!)
@out\unwrap!\tick!
apply_range = (range, val) ->
- if range.type == 'str'
+ if range\type! == 'str'
switch range\unwrap!
when 'raw' then val
when 'uni' then val / 128
diff --git a/lib/midi/init.moon b/lib/midi/init.moon
index b0f2f6a..0cbe5e0 100644
--- a/lib/midi/init.moon
+++ b/lib/midi/init.moon
@@ -1,5 +1,7 @@
import Value, Op from require 'core'
import input, output, inout, apply_range from require 'lib.midi.core'
+import ValueInput, EventInput from require 'core.base'
+import match from require 'core.pattern'
class gate extends Op
@doc: "(midi/gate port note [chan]) - gate from note-on and note-off messages"
@@ -7,18 +9,21 @@ class gate extends Op
new: =>
super 'bool', false
- setup: (params) =>
- super params
- @inputs[3] or= Value.num -1
- @assert_types 'midi/port', 'num', 'num'
- @impulses = { @inputs[1]\unwrap! }
+ setup: (inputs) =>
+ { port, note, chan } = match '=midi/port num num?', inputs
+ super
+ port: EventInput port.value!
+ note: ValueInput note
+ chan: ValueInput chan or Value.num -1
tick: =>
- local port, note, chan
- { port, note, chan } = [i\unwrap! for i in *@inputs]
+ { port, note, chan } = @inputs
+
+ if note\dirty! or chan\dirty!
+ @out\set false
for msg in port\receive!
- if msg.a == note and (chan == -1 or msg.chan == chan)
+ if msg.a == note! and (chan == -1 or msg.chan == chan!)
if msg.status == 'note-on'
@out\set true
elseif msg.status == 'note-off'
@@ -37,30 +42,25 @@ range can be one of:
new: =>
super 'num'
- destroy: =>
- dispatch\detach @mask if @mask
- setup: (params) =>
- super params
- @inputs[3] or= Value.num -1
- @inputs[4] or= Value.str 'uni'
- assert #@inputs == 4
- assert @inputs[4].type == 'num' or @inputs[4].type == 'str'
- @assert_types 'midi/port', 'num', 'num'
- @impulses = { @inputs[1]\unwrap! }
+ setup: (inputs) =>
+ { port, cc, chan, range } = match '=midi/port num num? any?', inputs
+ super
+ port: EventInput port.value!
+ cc: ValueInput cc
+ chan: ValueInput chan or Value.num -1
+ range: ValueInput range or Value.str 'uni'
if not @out\unwrap!
- @out\set apply_range @inputs[4], 0
+ @out\set apply_range @inputs.range, 0
tick: =>
- local port, cc, chan
- { port, cc, chan } = [i\unwrap! for i in *@inputs]
-
+ { port, cc, chan, range } = @inputs
for msg in port\receive!
if msg.status == 'control-change' and
- (chan == -1 or msg.chan == chan) and
- msg.a == cc
- @out\set apply_range @inputs[4], msg.b
+ (chan == -1 or msg.chan == chan!) and
+ msg.a == cc!
+ @out\set apply_range range, msg.b
{
:input
diff --git a/lib/time.moon b/lib/time.moon
index a726af3..007c4de 100644
--- a/lib/time.moon
+++ b/lib/time.moon
@@ -1,55 +1,50 @@
-import Registry, Value, Op from require 'core'
+import Registry, Value, Result, Op from require 'core'
+import ValueInput, EventInput from require 'core.base'
+import match from require 'core.pattern'
import monotime from require 'system'
-delta = do
- period = 1 / 60
-
- local last
- ->
-
class clock extends Op
new: =>
- super 'num'
- @impulses = { Registry.active!.kr }
- @out\set 0
+ super 'clock', { dt: 0, time: monotime! }
- setup: (params) =>
- super params
+ setup: =>
@last = monotime!
+ super kr: EventInput Registry.active!.kr
tick: =>
time = monotime!
dt = time - @last
if dt >= 1/60
- @out\set dt
+ @out\set :dt, :time
@last = time
class lfo extends Op
- @doc: "(lfo clock freq [wave]) - low-frequency oscillator
+ @doc: "(lfo [clock] freq [wave]) - low-frequency oscillator
oscillates between 0 and 1 at the frequency freq.
-wave selects the wave shape from the following (default sin):
-- sin
+wave selects the wave shape from the following:
+- sin (default)
- saw
- tri"
- tau = math.pi * 2
new: =>
super 'num'
@phase = 0
- default_wave = Value.str 'sin'
- setup: (params) =>
- super params
-
- @inputs[3] or= default_wave
- @assert_types 'num', 'num', 'str'
+ default_wave = Result value: Value.str 'sin'
+ setup: (inputs, scope) =>
+ { clock, freq, wave } = match 'clock? num any?', inputs
+ super
+ clock: EventInput clock or scope\get '*clock*'
+ freq: ValueInput freq
+ wave: ValueInput wave or default_wave
+ tau = math.pi * 2
tick: =>
- -- if clock is dirty
- if @inputs[1].dirty
- dt, freq, wave = @unwrap_inputs!
- @phase += dt * freq
+ if @inputs.clock\dirty!
+ { :clock, :freq, :wave } = @unwrap_all!
+
+ @phase += clock.dt * freq
@out\set switch wave
when 'sin' then .5 + .5 * math.cos @phase * tau
when 'saw' then @phase % 1
@@ -57,7 +52,7 @@ wave selects the wave shape from the following (default sin):
else error "unknown wave type"
class ramp extends Op
- @doc: "(ramp clock period [max]) - sawtooth lfo
+ @doc: "(ramp [clock] period [max]) - sawtooth lfo
ramps from 0 to max (default same as ramp) once every period seconds."
@@ -65,62 +60,68 @@ ramps from 0 to max (default same as ramp) once every period seconds."
super 'num'
@phase = 0
- setup: (params) =>
- super params
- assert @inputs[1].type == 'num', "tick requires a clock value"
- assert @inputs[2].type == 'num', "tick requires a period value"
+ setup: (inputs, scope) =>
+ { clock, period, max } = match 'clock? num num?', inputs
+ super
+ clock: EventInput clock or scope\get '*clock*'
+ period: ValueInput period
+ max: max and ValueInput max
tick: =>
- -- if clock is dirty
- if @inputs[1].dirty
- dt, period, max = @unwrap_inputs!
+ clock_dirty = @inputs.clock\dirty!
+ if clock_dirty
+ { :clock, :period, :max } = @unwrap_all!
max or= period
- @phase += dt / period
+ @phase += clock.dt / period
if @phase >= 1
@phase -= 1
+ if clock_dirty or (@inputs.max and @inputs.max\dirty!)
@out\set @phase * max
class tick extends Op
- @doc: "(tick clock period) - count ticks
+ @doc: "(tick [clock] period) - count ticks
counts upwards by one every period seconds and returns the number of completed ticks."
new: =>
- super 'num', 0
- @phase = 0
+ @phase, @count = 0, 0
+ super 'num', @count
- setup: (params) =>
- super params
- @assert_types 'num', 'num'
+ setup: (inputs, scope) =>
+ { clock, period } = match 'clock? num', inputs
+ super
+ clock: EventInput clock or scope\get '*clock*'
+ period: ValueInput period
- tick: (first) =>
- -- if clock is dirty
- if first or @inputs[1].dirty
- dt, period = @unwrap_inputs!
- @phase += dt / period
+ tick: =>
+ if @inputs.clock\dirty!
+ { :clock, :period, :max } = @unwrap_all!
+ @phase += clock.dt / period
- next_num = math.floor @phase
- if next_num != @.out!
- @out\set next_num
+ if @phase >= 1
+ @phase -= 1
+ @count += 1
+ @out\set @count
class every extends Op
- @doc: "(every clock period) - trigger every period seconds
+ @doc: "(every [clock] period) - trigger every period seconds
returns true once every period seconds."
new: =>
super 'bang'
@phase = 0
- setup: (@period) =>
- super params
- @assert_types 'num', 'num'
+ setup: (inputs, scope) =>
+ { clock, period } = match 'clock? num', inputs
+ super
+ clock: EventInput clock or scope\get '*clock*'
+ period: ValueInput period
- tick: (first) =>
- -- if clock is dirty
- if first or @inputs[1].dirty
- dt, period = @unwrap_inputs!
- @phase += dt / period
+ tick: =>
+ if @inputs.clock\dirty!
+ { :clock, :period, :max } = @unwrap_all!
+ @phase += clock.dt / period
if @phase >= 1
@phase -= 1
diff --git a/spec/core/pattern_spec.moon b/spec/core/pattern_spec.moon
index ee56634..c218dd2 100644
--- a/spec/core/pattern_spec.moon
+++ b/spec/core/pattern_spec.moon
@@ -4,7 +4,7 @@ import Result, Value from require 'core.value'
-- wrap in non-const result
wrap = (value) ->
with Result :value
- .all_impulses = { 'fake' }
+ .side_inputs = { 'fake' }
-- wrap in const result
wrap_const = (value) -> Result :value
@@ -157,9 +157,9 @@ describe 'match', ->
c_str = wrap_const Value.str 'hello'
it 'matches lists', ->
- assert.is.same {num, num, str}, {match 'num num str', {num, num, str}}
- assert.is.same {num, str}, {match 'num str', {num, str}}
- assert.is.same {c_num, str}, {match '=num str', {c_num, str}}
+ assert.is.same {num, num, str}, match 'num num str', {num, num, str}
+ assert.is.same {num, str}, match 'num str', {num, str}
+ assert.is.same {c_num, str}, match '=num str', {c_num, str}
it 'throws type errors', ->
assert.has.error -> match 'str num str', {num, num, str}
@@ -172,18 +172,18 @@ describe 'match', ->
assert.has.error -> match '*num', {num, num, str}
it 'matches optional arguments', ->
- assert.is.same {str, num, str}, {match 'str num? str', {str, num, str}}
- assert.is.same {str, false, str}, {match 'str num? str', {str, str}}
- assert.is.same {str, false}, {match 'str num?', {str}}
+ assert.is.same {str, num, str}, match 'str num? str', {str, num, str}
+ assert.is.same {str, nil, str}, match 'str num? str', {str, str}
+ assert.is.same {str, nil}, match 'str num?', {str}
it 'matches splats', ->
- assert.is.same {{c_str, str, str}, num}, {match '*str num', {c_str, str, str, num}}
- assert.is.same {c_str, {str, str}}, {match 'any? *str', {c_str, str, str}}
+ assert.is.same {{c_str, str, str}, num}, match '*str num', {c_str, str, str, num}
+ assert.is.same {c_str, {str, str}}, match 'any? *str', {c_str, str, str}
assert.has.error -> match '*str num', {num}
assert.has.error -> match 'any? *str', {str}
it 'matches optional splats', ->
- assert.is.same {{c_str, str, str}, num}, {match '*str? num', {c_str, str, str, num}}
- assert.is.same {c_str, {str, str}}, {match 'any? *str?', {c_str, str, str}}
- assert.is.same {{}, num}, {match '*str? num', {num}}
- assert.is.same {str, {}}, {match 'any? *str?', {str}}
+ assert.is.same {{c_str, str, str}, num}, match '*str? num', {c_str, str, str, num}
+ assert.is.same {c_str, {str, str}}, match 'any? *str?', {c_str, str, str}
+ assert.is.same {{}, num}, match '*str? num', {num}
+ assert.is.same {str, {}}, match 'any? *str?', {str}