diff options
| author | s-ol <s-ol@users.noreply.github.com> | 2020-03-01 17:44:56 +0000 |
|---|---|---|
| committer | s-ol <s-ol@users.noreply.github.com> | 2020-03-01 17:46:04 +0000 |
| commit | a1312100fad100190e20b1b3443b5cd962dc8999 (patch) | |
| tree | 8fc6194fc4b326ee39a9ca6d798cd208664f5fc3 | |
| parent | add type-pattern matching language (diff) | |
| download | alive-a1312100fad100190e20b1b3443b5cd962dc8999.tar.gz alive-a1312100fad100190e20b1b3443b5cd962dc8999.zip | |
new op interface part one
| -rw-r--r-- | core/base.moon | 77 | ||||
| -rw-r--r-- | core/cell.moon | 4 | ||||
| -rw-r--r-- | core/invoke.moon | 8 | ||||
| -rw-r--r-- | core/pattern.moon | 4 | ||||
| -rw-r--r-- | core/registry.moon | 6 | ||||
| -rw-r--r-- | core/value.moon | 33 | ||||
| -rw-r--r-- | lib/debug.moon | 16 | ||||
| -rw-r--r-- | lib/midi/core.moon | 66 | ||||
| -rw-r--r-- | lib/midi/init.moon | 50 | ||||
| -rw-r--r-- | lib/time.moon | 119 | ||||
| -rw-r--r-- | spec/core/pattern_spec.moon | 26 |
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} |
