diff options
| author | s-ol <s-ol@users.noreply.github.com> | 2020-02-19 21:17:46 +0000 |
|---|---|---|
| committer | s-ol <s-ol@users.noreply.github.com> | 2020-02-19 21:17:53 +0000 |
| commit | e62ef99244bb3aa906810a5cfd53c0dac68b0f37 (patch) | |
| tree | e7379e2add7014e7124f678f4a7a8edc699fe914 | |
| parent | major refactoring: Const, Stream + ResultNode (diff) | |
| download | alive-e62ef99244bb3aa906810a5cfd53c0dac68b0f37.tar.gz alive-e62ef99244bb3aa906810a5cfd53c0dac68b0f37.zip | |
merge Const and Stream into Value; Dataflow logic
lib not fully ported / functonial
| -rw-r--r-- | copilot.moon | 14 | ||||
| -rw-r--r-- | core/base.moon | 37 | ||||
| -rw-r--r-- | core/builtin.moon | 36 | ||||
| -rw-r--r-- | core/cell.moon | 10 | ||||
| -rw-r--r-- | core/init.moon | 6 | ||||
| -rw-r--r-- | core/invoke.moon | 13 | ||||
| -rw-r--r-- | core/parsing.moon | 10 | ||||
| -rw-r--r-- | core/registry.moon | 58 | ||||
| -rw-r--r-- | core/scope.moon | 17 | ||||
| -rw-r--r-- | core/value.moon | 154 | ||||
| -rw-r--r-- | init.moon | 23 | ||||
| -rw-r--r-- | lib/debug.moon | 9 | ||||
| -rw-r--r-- | lib/midi/core.moon | 147 | ||||
| -rw-r--r-- | lib/midi/init.moon | 75 | ||||
| -rw-r--r-- | lib/midi/launchctl.moon | 59 | ||||
| -rw-r--r-- | lib/osc.moon | 29 | ||||
| -rw-r--r-- | lib/time.moon | 139 | ||||
| -rw-r--r-- | lib/util.moon | 114 | ||||
| -rw-r--r-- | spec/cell_spec.moon | 18 | ||||
| -rw-r--r-- | spec/const_spec.moon | 122 | ||||
| -rw-r--r-- | spec/parsing_spec.moon | 30 | ||||
| -rw-r--r-- | spec/scope_spec.moon | 48 | ||||
| -rw-r--r-- | spec/value_spec.moon | 112 | ||||
| -rw-r--r-- | test.alv | 19 |
24 files changed, 714 insertions, 585 deletions
diff --git a/copilot.moon b/copilot.moon index 78848e5..a66c570 100644 --- a/copilot.moon +++ b/copilot.moon @@ -29,20 +29,21 @@ class Copilot return scope = Scope ast, globals - ok, err = pcall (@registry\wrap ast\eval), scope, @registry + ok, err = pcall ast\eval, scope, @registry if not ok L\error "error evaluating: #{err}" return @root = err + ast - spit @file, ast\stringify! - - update: (dt) => + tick: => @poll! if @root - @root\update dt + ok, err = pcall @registry\wrap_tick @root\tick + if not ok + L\error err tb = (msg) -> debug.traceback msg, 2 poll: => @@ -52,7 +53,8 @@ class Copilot if @last_modification < modification L\log "#{@file} changed at #{modification}" - L\push @\patch + ast = L\push @registry\wrap_eval @\patch + spit @file, ast\stringify! @last_modification = os.time! :Copilot diff --git a/core/base.moon b/core/base.moon index 1b1be5d..60ee460 100644 --- a/core/base.moon +++ b/core/base.moon @@ -1,21 +1,47 @@ -- base definitions for extensions +import Value from require 'core.value' + +unpack or= table.unpack -- a persistent expression Operator -- -- accepts Const or Stream inputs and produces a Stream output class Op - update: (dt) => + new: (type, init) => + @impulses = {} + + if type + @out = Value type, init + + -- (re)-initialize this Op with the given inputs + -- 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) => - -- set @out to a Value (Const or Stream) - setup: (...) => + -- called once per frame if any inputs or impulses are dirty, and once + -- immediately after :setup(). 'first' will be true in the latter case. + -- Should update @out. + tick: (first) => + -- called once when the Op is destroyed destroy: => +-- utilities + unwrap_inputs: => + unpack [input! for input in *@inputs] + + assert_types: (...) => + num = select '#', ... + assert #@inputs >= num, "argument count mismatch" + for i = 1, num + expect = select i, ... + assert @inputs[i].type == expect, "expected argument #{i} of #{@} to be of type #{expect} but found #{@inputs[i]}" + -- static __tostring: => "<op: #{@@__name}>" __inherited: (cls) => cls.__base.__tostring = @__tostring - -- a builtin / special form / cell-evaluation strategy -- -- responsible for quoting/evaluating subexpressions, @@ -51,7 +77,7 @@ class Action -- static -- find & patch the action for the expression with Tag 'tag' if it exists, -- and is compatible with the new Cell contents, otherwise instantiate it. - -- register the action with the tag, evaluate it and return the ResultNode + -- register the action with the tag, evaluate it and return the Result @eval_cell: (scope, tag, head, tail) => last = tag\last! compatible = last and @@ -93,6 +119,7 @@ class FnDef "(fn (#{table.concat [p\stringify! for p in *@params], ' '}) ...)" { + :Dispatcher :Op :Action :FnDef diff --git a/core/builtin.moon b/core/builtin.moon index 71beac3..6abcbd9 100644 --- a/core/builtin.moon +++ b/core/builtin.moon @@ -1,6 +1,6 @@ -- builtin special forms import Action, FnDef from require 'core.base' -import ResultNode, Value, Const from require 'core.value' +import Result, Value from require 'core.value' import Cell from require 'core.cell' import Scope from require 'core.scope' @@ -13,7 +13,7 @@ prints the docstring for sym in the console" assert #tail == 1, "'doc' takes exactly one parameter" def = L\push tail[1]\eval, scope - with ResultNode children: { def } + with Result children: { def } def = def.value\const!\unwrap! L\print "(doc #{tail[1]\stringify!}):\n#{def.doc}\n" @@ -35,9 +35,9 @@ updates all val-exprs." name = (name\quote scope)\unwrap 'sym' with val_expr\eval scope - scope\set name, .value + scope\set name, \make_ref! - ResultNode :children + Result :children class use extends Action @doc: "(use scope1 [scope2]...) - merge scopes into parent scope @@ -49,10 +49,10 @@ all scopes have to be eval-time constants." L\trace "evaling #{@}" for child in *tail result = L\push child\eval, scope - value = result\value_only!\const! + value = result\const! scope\use value\unwrap 'scope', "'use' only works on scopes" - ResultNode! + Result! class require_ extends Action @doc: "(require name-str) - require a module @@ -65,10 +65,10 @@ name-str has to be an eval-time constant." assert #tail == 1, "'require' takes exactly one parameter" result = L\push tail[1]\eval, scope - name = result\value_only!\const! + name = result\const! L\trace @, "loading module #{name}" - ResultNode value: Value.wrap require "lib.#{name\unwrap 'str'}" + Result value: Value.wrap require "lib.#{name\unwrap 'str'}" class import_ extends Action @doc: "(import sym1 [sym2]...) - require and define modules @@ -81,9 +81,9 @@ requires modules sym1, sym2, ... and defines them as sym1, sym2, ... in the curr for child in *tail name = (child\quote scope)\unwrap 'sym' - scope\set name, Value.wrap require "lib.#{name}" + scope\set name, Result value: Value.wrap require "lib.#{name}" - ResultNode! + Result! class import_star extends Action @doc: "(import* sym1 [sym2]...) - require and use modules @@ -99,7 +99,7 @@ requires modules sym1, sym2, ... and merges them into the current scope" name = (child\quote scope)\unwrap 'sym' scope\use (Value.wrap require "lib.#{name}")\unwrap 'scope' - ResultNode! + Result! class fn extends Action @doc: "(fn (p1 [p2]...) body-expr) - declare a (lambda) function @@ -117,7 +117,7 @@ the symbols p1, p2, ... will resolve to the arguments passed to the function." param\quote scope body = body\quote scope - ResultNode value: Value.wrap FnDef param_symbols, body, scope + Result value: Value.wrap FnDef param_symbols, body, scope class defn extends Action @doc: "(defn name-sym (p1 [p2]...) body-expr) - define a function @@ -138,8 +138,8 @@ declares a lambda (see (doc fn)) and defines it in the current scope" body = body\quote scope fn = FnDef param_symbols, body, scope - scope\set name, Value.wrap fn - ResultNode! + scope\set name, Result value: Value.wrap fn + Result! class do_expr extends Action @doc: "(do expr1 [expr2]...) - update multiple expressions @@ -148,7 +148,7 @@ evaluates and continously updates expr1, expr2, ... the last expression's value is returned." eval: (scope, tail) => - ResultNode children: [expr\eval scope for expr in *tail] + Result children: [expr\eval scope for expr in *tail] class if_ extends Action @doc: "(if bool then-expr [else-xpr]) - make an eval-time const choice @@ -164,7 +164,7 @@ to then-expr, otherwise it is equivalent to else-xpr if given, or nil otherwise. { xif, xthen, xelse } = tail xif = L\push xif\eval, scope - xif = xif\value_only!\const!\unwrap! + xif = xif\const!\unwrap! if xif xthen\eval scope @@ -189,8 +189,8 @@ class trace extends Action import: import_ 'import*': import_star - true: Const.bool true - false: Const.bool false + true: Value.bool true + false: Value.bool false :fn, :defn 'do': do_expr diff --git a/core/cell.moon b/core/cell.moon index 642e1a6..0cb8e9f 100644 --- a/core/cell.moon +++ b/core/cell.moon @@ -1,5 +1,5 @@ -- ALV Cell type -import Const from require 'core.value' +import Value from require 'core.value' import op_invoke, fn_invoke from require 'core.invoke' import Tag from require 'core.tag' @@ -11,7 +11,8 @@ class Cell -- white: optional sequence of whitespace segments ([0 .. #@children]) new: (@tag=Tag.blank!, @children, @white) => if not @white - @white = ['' for i=1,#@children+1] + @white = ['' for i=1,#@children] + @white[0] = '' assert #@white == #@children, "mismatched whitespace length" @@ -22,8 +23,7 @@ class Cell -- AST interface eval: (scope) => - head_result = @head!\eval scope - head = head_result.value\const! + head = (@head!\eval scope)\const! Action = switch head.type when 'opdef' -- scope\get 'op-invoke' @@ -89,7 +89,7 @@ class Cell -- -- evaluates with an implicit 'do' in the head class RootCell extends Cell - head: => Const.sym 'do' + head: => Value.sym 'do' tail: => @children stringify: => diff --git a/core/init.moon b/core/init.moon index b885747..a309722 100644 --- a/core/init.moon +++ b/core/init.moon @@ -2,7 +2,7 @@ L or= setmetatable {}, __index: => -> import Op, Action, FnDef from require 'core.base' -import Stream, Const, load_ from require 'core.value' +import Value, Result, load_ from require 'core.value' import Scope from require 'core.scope' load_! @@ -15,7 +15,7 @@ import cell, program from require 'core.parsing' globals = Scope.from_table require 'core.builtin' { - :Stream, :Const + :Value, :Result :Cell, :RootCell :Op, :Action, :FnDef :Scope @@ -30,5 +30,5 @@ globals = Scope.from_table require 'core.builtin' ast = assert (cell\match str), "failed to parse: #{str}" result = ast\eval scope - result\value_only! + result\const! } diff --git a/core/invoke.moon b/core/invoke.moon index 5a6c420..a3932f8 100644 --- a/core/invoke.moon +++ b/core/invoke.moon @@ -1,4 +1,4 @@ -import ResultNode, Value from require 'core.value' +import Result, Value from require 'core.value' import Action from require 'core.base' import Scope from require 'core.scope' @@ -8,16 +8,17 @@ class op_invoke extends Action @op\destroy! if @op - def = head\const!\unwrap 'opdef', "cant op-invoke #{@head}" + def = head\unwrap 'opdef', "cant op-invoke #{@head}" @head, @op = head, def! true eval: (scope, tail) => children = L\push -> [L\push expr\eval, scope for expr in *tail] - value = @op\setup unpack [child.value for child in *children] + @op\setup [child.value for child in *children] + @op\tick true - ResultNode :children, :value, op: @op + Result :children, value: @op.out, op: @op class fn_invoke extends Action -- @TODO: @@ -32,7 +33,7 @@ class fn_invoke extends Action true eval: (outer_scope, tail) => - { :params, :body, :scope } = @head\const!\unwrap 'fndef', "cant fn-invoke #{@head}" + { :params, :body, :scope } = @head\unwrap 'fndef', "cant fn-invoke #{@head}" assert #params == #tail, "argument count mismatch in #{@head}" @@ -47,7 +48,7 @@ class fn_invoke extends Action result = body\eval fn_scope table.insert children, result - ResultNode :children, value: result.value + Result :children, value: result.value { :op_invoke, :fn_invoke diff --git a/core/parsing.moon b/core/parsing.moon index 96d8125..03ef85c 100644 --- a/core/parsing.moon +++ b/core/parsing.moon @@ -1,4 +1,4 @@ -import Const from require 'core.value' +import Value from require 'core.value' import Cell, RootCell from require 'core.cell' import Tag from require 'core.tag' import R, S, P, V, C, Ct from require 'lpeg' @@ -16,15 +16,15 @@ mspace = (comment + wc)^0 / 1 -- optional whitespace -- atoms digit = R '09' first = (R 'az', 'AZ') + S '-_+*/.!?=' -sym = first * (first + digit)^0 / Const\parse 'sym' +sym = first * (first + digit)^0 / Value\parse 'sym' -strd = '"' * (C ((P '\\"') + (P '\\\\') + (1 - P '"'))^0) * '"' / Const\parse 'str', '\"' -strq = "'" * (C ((P "\\'") + (P '\\\\') + (1 - P "'"))^0) * "'" / Const\parse 'str', '\'' +strd = '"' * (C ((P '\\"') + (P '\\\\') + (1 - P '"'))^0) * '"' / Value\parse 'str', '\"' +strq = "'" * (C ((P "\\'") + (P '\\\\') + (1 - P "'"))^0) * "'" / Value\parse 'str', '\'' str = strd + strq int = digit^1 float = (digit^1 * '.' * digit^0) + (digit^0 * '.' * digit^1) -num = (float + int) / Const\parse 'num' +num = (float + int) / Value\parse 'num' atom = num + sym + str diff --git a/core/registry.moon b/core/registry.moon index 4dd5bbb..e8272fa 100644 --- a/core/registry.moon +++ b/core/registry.moon @@ -1,7 +1,12 @@ +import Value from require 'core.value' + class Registry new: () => @map = {} + @tick = 0 + @kr = Value.bool true + -- methods for Tag last: (index) => @last_map[index] @@ -15,37 +20,44 @@ class Registry L\trace "reg: init pending to #{expr}" table.insert @pending, { :tag, :expr } - active: -> - assert Registry.active_registry, "no active Registry!" + active: -> assert Registry.active_registry, "no active Registry!" -- public methods - wrap: (fn) => - (...) -> - @prepare! - with fn ... - @finalize! - - prepare: => - assert not @prev, "already have a previous registry? #{@prev}" - @prev, @@active_registry = @@active_registry, @ + wrap_eval: (fn) => (...) -> + @grab! @last_map, @map, @pending = @map, {}, {} - finalize: => - for tag, val in pairs @last_map - if not @map[tag] - val\destroy! + with fn ... + for tag, val in pairs @last_map + if not @map[tag] + val\destroy! - for { :tag, :expr } in *@pending - -- tag was solved by another pending registration - -- (e.g. first [A] is solved, then [5.A] is solved) - continue if tag\index! + for { :tag, :expr } in *@pending + -- tag was solved by another pending registration + -- (e.g. first [A] is solved, then [5.A] is solved) + continue if tag\index! - next_tag = @next_tag! - L\trace "assigned new tag #{next_tag} to #{tag} #{expr}" - tag\set next_tag - @map[tag\index!] = expr + next_tag = @next_tag! + L\trace "assigned new tag #{next_tag} to #{tag} #{expr}" + tag\set next_tag + @map[tag\index!] = expr + + @release! + + wrap_tick: (fn) => (...) -> + @grab! + @tick += 1 + @kr\set true + + with fn ... + @release! + + grab: => + assert not @prev, "already have a previous registry? #{@prev}" + @prev, @@active_registry = @@active_registry, @ + release: => assert @ == @@active_registry, "not the active registry!" @@active_registry, @prev = @prev, nil diff --git a/core/scope.moon b/core/scope.moon index aaa38f2..f795f97 100644 --- a/core/scope.moon +++ b/core/scope.moon @@ -1,12 +1,16 @@ -import Value from require 'core.value' +import Result, Value from require 'core.value' class Scope new: (@node, @parent) => @values = {} - set_raw: (key, val) => @values[key] = Value.wrap val, key + set_raw: (key, val) => + value = Value.wrap val, key + @values[key] = Result :value + set: (key, val) => L\trace "setting #{key} = #{val} in #{@}" + assert val.__class == Result, "expected #{key}=#{val} to be Result" @values[key] = val get: (key, prefix='') => @@ -20,9 +24,9 @@ class Scope if not start return @parent and L\push -> @parent\get key - scope = @get start - assert scope and scope.type == 'scope', "cant find '#{prefix}#{start}' for '#{prefix}#{key}'" - scope\unwrap!\get rest, "#{prefix}#{start}/" + child = @get start + assert child and child.value.type == 'scope', "#{start} is not a scope (looking for #{key})" + child.value\unwrap!\get rest, "#{prefix}#{start}/" use: (other) => L\trace "using defs from #{other} in #{@}" @@ -31,7 +35,8 @@ class Scope from_table: (tbl) -> with Scope! - .values = { k, Value.wrap v, k for k,v in pairs tbl } + for k, v in pairs tbl + \set_raw k, v __tostring: => buf = "<Scope" diff --git a/core/value.moon b/core/value.moon index f3f9796..986c846 100644 --- a/core/value.moon +++ b/core/value.moon @@ -1,9 +1,9 @@ -- ALV Value types -import Op, Action, FnDef from require 'core.base' - -local Scope +local Scope, Registry, Op, Action, FnDef load_ = -> import Scope from require 'core.scope' + import Registry from require 'core.registry' + import Op, Action, FnDef from require 'core.base' ancestor = (klass) -> assert klass, "cant find the ancestor of nil" @@ -16,26 +16,73 @@ ancestor = (klass) -> -- - a Value -- - an Op (to update) -- - children (results of subexpressions that were evaluated) +-- - cached list of all Dispatchers affecting all Ops in the subtree -- --- ResultNodes form a tree that controls execution order and message passing +-- Results form a tree that controls execution order and message passing -- between Ops. -class ResultNode +class Result -- params: table with optional keys op, value, children new: (params={}) => - @op = params.op @value = params.value + @op = params.op @children = params.children or {} - -- unwrap value and assert that neither @op nor @children were set - value_only: (msg) => - assert not @op and #@children == 0, msg or "pure expression expected" + @all_impulses = {} + for child in *@children + for d in pairs child.all_impulses + @all_impulses[d] = true + + if @op + assert @op.impulses, "#{@op} not set up correctly (impulses)" + for d in *@op.impulses + @all_impulses[d] = true + + -- asserts value-constness and returns the value + const: (msg) => + assert not (next @all_impulses), msg or "eval-time const expected" @value - update: (dt) => + -- 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 + + -- 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! + any_dirty = true + break + + -- early-out if no streams are dirty in this whole subtree + return unless any_dirty + for child in *@children - child\update dt - @op\update dt if @op + child\tick! + + if @op + -- 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 + if stream\dirty! + self_dirty = true + break + + L\trace "#{op} is #{if self_dirty then 'dirty' else 'clean'}" + return unless self_dirty + + @op\tick! +-- static __tostring: => buf = "<result=#{@value}" buf ..= " #{@op}" if @op @@ -43,19 +90,18 @@ class ResultNode buf ..= ">" buf -local Const, Stream - -- ALV Type wrapper class Value -- @type - type name. -- builtin types: * literals: sym, num, bool -- * scope, opdef, fndef, builtin -- @value - Lua value - access through :unwrap() - new: (@type, @value) => + new: (@type, @value, @raw) => + @updated = 0 - -- asserts value-constness - -- returns self (for chaining) - const: => error 'not a constant' + dirty: => @updated == Registry.active!.tick + + set: (@value) => @updated = Registry.active!.tick -- unwrap to the Lua type -- asserts @type == type, msg if given @@ -63,14 +109,30 @@ class Value assert type == @type, msg or "#{@} is not a #{type}" if type @value +-- AST interface + eval: (scope) => + switch @type + when 'num', 'str' + Result value: @ + when 'sym' + assert (scope\get @value), "undefined reference to symbol '#{@value}'" + else + error "cannot evaluate #{@}" + + quote: => @ + + stringify: => assert @raw, "stringifying Value that wasn't parsed" + + clone: (prefix) => @ + -- in case of doubt: + -- clone: (prefix) => Value @type, @value, @raw + -- static __tostring: => value = if 'table' == (type @value) and rawget @value, '__base' then @value.__name else @value "<#{@@__name} #{@type}: #{value}>" + __call: (...) => @unwrap ... __eq: (other) => other.type == @type and other.value == @value - __inherited: (cls) => - cls.__base.__tostring = @__tostring - cls.__base.__eq = @__eq -- wrap a Lua type @wrap: (val, name='(unknown)') -> @@ -96,62 +158,26 @@ class Value error "#{name}: cannot wrap '#{val.__class.__name}' instance" else -- plain table - return Const 'scope', Scope.from_table val + return Value 'scope', Scope.from_table val else error "#{name}: cannot wrap Lua type '#{type val}'" - Const typ, val - -class Stream extends Value - new: (@type, @value=nil) => - - set: (@value) => - -- set dirty flag (?) - -class Const extends Value - new: (type, value, @raw) => - super type, value - --- Value interface - const: => @ - --- AST interface - eval: (scope) => - value = switch @type - when 'num', 'str' - @ - when 'sym' - assert (scope\get @value), "undefined reference to symbol '#{@value}'" - else - error "cannot evaluate #{@}" - - ResultNode :value - - quote: => @ - - stringify: => @raw + Value typ, val - clone: (prefix) => @ - -- in case of doubt: - -- clone: (prefix) => Const @type, @value, @raw - --- static unescape = (str) -> str\gsub '\\([\'"\\])', '%1' - @parse: (type, sep) => switch type when 'num' then (match) -> @ 'num', (tonumber match), match when 'sym' then (match) -> @ 'sym', match, match when 'str' then (match) -> @ 'str', (unescape match), sep .. match .. sep - @num: (num) -> Const 'num', num, tostring num - @str: (str) -> Const 'str', str, "'#{str}'" - @sym: (sym) -> Const 'sym', sym, sym - @bool: (bool) -> Const 'bool', bool, tostring bool + @num: (num) -> Value 'num', num, tostring num + @str: (str) -> Value 'str', str, "'#{str}'" + @sym: (sym) -> Value 'sym', sym, sym + @bool: (bool) -> Value 'bool', bool, tostring bool { - :ResultNode + :Result :Value - :Stream, :Const :load_ } @@ -1,7 +1,7 @@ -- run from CLI -import monotime, sleep from require 'system' import Logger from require 'logger' import Copilot from require 'copilot' +import sleep from require 'system' arguments, key = {} for a in *arg @@ -16,25 +16,8 @@ for a in *arg Logger.init arguments.log -delta = do - period = 1 / 60 - - local last - -> - if last - target, current = (last + period), monotime! - if current > target - L\warn 'Frame Skipped!' - else - sleep target - current - - time = monotime! - with time - (last or time) - last = time - copilot = Copilot arguments[1] while true - dt = delta! - - copilot\update dt + copilot\tick! + sleep 1 / 1000 diff --git a/lib/debug.moon b/lib/debug.moon index f642c2e..08a7f78 100644 --- a/lib/debug.moon +++ b/lib/debug.moon @@ -3,10 +3,13 @@ import Op from require 'core' class out extends Op @doc: "(out name-str value) - log value to the console" - setup: (@name, @value) => + setup: (params) => + super params + assert @inputs[2], "need a value" + @assert_types 'str', @inputs[2].type - update: (dt) => - L\print "#{@name\unwrap 'str'}", @value\unwrap! + tick: => + L\print @unwrap_inputs! { :out diff --git a/lib/midi/core.moon b/lib/midi/core.moon index 891de0a..7f661ed 100644 --- a/lib/midi/core.moon +++ b/lib/midi/core.moon @@ -1,5 +1,6 @@ import RtMidiIn, RtMidiOut, RtMidi from require 'luartmidi' import band, bor, lshift, rshift from require 'bit32' +import Op, Registry from require 'core' MIDI = { [0x9]: 'note-on' @@ -15,75 +16,101 @@ MIDI = { rMIDI = {v,k for k,v in pairs MIDI} -class Input - new: (name) => - @input = RtMidiIn RtMidi.Api.UNIX_JACK - +find_port = (Klass, name) -> + with Klass RtMidi.Api.UNIX_JACK id = nil - for port=1,@input\getportcount! - if name == @input\getportname port + for port=1, \getportcount! + if name == \getportname port id = port break - @input\openport id + \openport id + +class MidiPort + new: (@inp, @out) => - @listeners = {} + dirty: => + @updated == Registry.active!.tick tick: => - while true - delta, bytes = @input\getmessage! - break unless delta - - { status, a, b } = bytes - chan = band status, 0xf - status = MIDI[rshift status, 4] - @dispatch status, chan, a, b - - dispatch: (status, chan, a, b) => - L\trace "dispatching MIDI event #{status} CH#{chan} #{a} #{b}" - for mask, handler in pairs @listeners - match = true - match and= status == mask.status if mask.status - match and= chan == mask.chan if mask.chan - match and= a == mask.a if mask.a - if match - handler status, chan, a, b - - -- register a handler - -- mask is { :status, :chan, :a } (all keys optional) - -- returns mask for op convenience - attach: (mask, handler) => - @listeners[mask] = handler - mask - - detach: (mask) => - @listeners[mask] = nil - -class Output - new: (name) => - @output = RtMidiOut RtMidi.Api.UNIX_JACK + if @inp + @messages = while true + delta, bytes = @inp\getmessage! + break unless delta + + { status, a, b } = bytes + chan = band status, 0xf + status = MIDI[rshift status, 4] + { :status, :chan, :a, :b } + + if @messages + @updated = Registry.active!.tick + + receive: => + assert @inp, "#{@} is not an input port" + return unless @messages + coroutine.wrap -> + for msg in *@messages + coroutine.yield msg - id = nil - for port=1,@output\getportcount! - if name == @output\getportname port - id = port - break + send: (status, chan, a, b) => + assert @out, "#{@} is not an output port" + if 'string' == type 'status' + status = bor (lshift rMIDI[status], 4), chan + @out\sendmessage status, a, b - @output\openport id +class input extends Op + @doc: "(midi/input name) - create a MIDI input port" - send: (status, chan, a, b) => - status = bor (lshift rMIDI[status], 4), chan - @output\sendmessage status, a, b + new: => + super 'midi/port' + @impulses = { Registry.active!.kr } + + setup: (params) => + super params + @assert_types 'str' + + tick: (first) => + { name } = @inputs + if first or name\dirty! + @out\set MidiPort find_port RtMidiIn, name\unwrap! + + @out\unwrap!\tick! + +class output extends Op + @doc: "(midi/output name) - create a MIDI output port" + + new: => + super 'midi/port' + + setup: (params) => + super params + @assert_types 'str' + + tick: (first) => + { name } = @inputs + if first or name\dirty! + @out\set MidiPort nil, find_port RtMidiOut, name\unwrap! + +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' + + tick: (first) => + { inp, out } = @inputs -class InOut - new: (inp, out) => - @inp = Input inp - @out = Output out + 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\tick ... - attach: (...) => @inp\attach ... - detach: (...) => @inp\detach ... - send: (...) => @out\send ... + @out\unwrap!\tick! apply_range = (range, val) -> if range.type == 'str' @@ -100,8 +127,8 @@ apply_range = (range, val) -> error "range has to be a string or number" { - :Input - :Output - :InOut + :input + :output + :inout :apply_range } diff --git a/lib/midi/init.moon b/lib/midi/init.moon index c9e2476..03f5115 100644 --- a/lib/midi/init.moon +++ b/lib/midi/init.moon @@ -1,34 +1,31 @@ -import Stream, Const, Op from require 'core' -import Input, apply_range from require 'lib.midi.core' - -dispatch = Input 'system:midi_capture_4' +import Value, Op from require 'core' +import input, output, inout, apply_range from require 'lib.midi.core' class gate extends Op - @doc: "(midi/gate note [chan]) - gate from note-on and note-off messages" + @doc: "(midi/gate port note [chan]) - gate from note-on and note-off messages" - destroy: => - dispatch\detach @mask if @mask - - setup: (note, chan) => - dispatch\detach @mask if @mask + new: => + super 'bool', false - note = note\const!\unwrap 'num' - chan = chan and chan\const!\unwrap 'num' - @value = false + setup: (params) => + super params + @inputs[3] or= Value.num -1 + @assert_types 'midi/port', 'num', 'num' + @impulses = { @inputs[1]\unwrap! } - @mask = dispatch\attach { :chan, a: note }, (status) -> - if status == 'note-on' - @out\set true - else if status == 'note-off' - @out\set false + tick: => + local port, note, chan + { port, note, chan } = [i\unwrap! for i in *@inputs] - @out = Stream 'bool', false - @out - - update: (dt) => dispatch\tick! + for msg in port\receive! + if msg.a == note and (chan == -1 or msg.chan == chan) + if msg.status == 'note-on' + @out\set true + elseif msg.status == 'note-off' + @out\set false class cc extends Op - @doc: "(midi/cc cc [chan [range]]) - MIDI CC to number + @doc: "(midi/cc port cc [chan [range]]) - MIDI CC to number range can be one of: - 'raw' [ 0 - 128[ @@ -37,24 +34,36 @@ range can be one of: - 'rad' [ 0 - tau[ - (num) [ 0 - num[" - destroy: => - dispatch\detach @mask if @mask + new: => + super 'num' - setup: (cc, chan, @range=Const.str'uni') => + destroy: => dispatch\detach @mask if @mask - cc = cc\const!\unwrap 'num' - chan = chan and chan\const!\unwrap 'num' + setup: (params) => + super params + @inputs[3] or= Value.num -1 + @inputs[4] or= Value.str 'uni' + @assert_types 'midi/port', 'num', 'num', 'str' + @impulses = { @inputs[1]\unwrap! } - @mask = dispatch\attach { status: 'control-change', :chan, a: cc }, (_, _, _, val) -> - @out\set apply_range @range, val + if not @out\unwrap! + @out\set apply_range @inputs[4], 0 - @out = Stream 'num', 0 - @out + tick: => + local port, cc, chan + { port, cc, chan } = [i\unwrap! for i in *@inputs] - update: (dt) => dispatch\tick! + 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 { + :input + :output + :inout :gate :cc } diff --git a/lib/midi/launchctl.moon b/lib/midi/launchctl.moon index 778a724..c0d5e24 100644 --- a/lib/midi/launchctl.moon +++ b/lib/midi/launchctl.moon @@ -1,13 +1,12 @@ -import Stream, Const, Op from require 'core' -import InOut, apply_range from require 'lib.midi.core' +import Value, Op from require 'core' +import apply_range from require 'lib.midi.core' import bor, lshift from require 'bit32' -launch = InOut 'system:midi_capture_4', 'system:midi_playback_4' - +unpack or= table.unpack color = (r, g) -> bor 12, r, (lshift g, 4) class cc_seq extends Op - @doc: "(launctl/cc-seq i start chan [steps [range]]) - CC-Sequencer + @doc: "(launctl/cc-seq port i start chan [steps [range]]) - CC-Sequencer returns the value for the i-th step steps (buttons starting from start). steps defaults to 8. @@ -19,43 +18,41 @@ range can be one of: - 'rad' [ 0 - tau[ - (num) [ 0 - num[" - destroy: => - launch\detach @mask if @mask - new: => + super 'num' @steps = {} - @out = Stream 'num' - setup: (@i, start, chan, steps=(Const.num 8), @range=Const.str 'uni') => - launch\detach @mask if @mask + setup: (params) => + super params - @start = start\const!\unwrap 'num' - @chan = chan\const!\unwrap 'num' - steps = steps\const!\unwrap 'num' + @inputs[5] or= Value.num 8 + @inputs[6] or= Value.str 'uni' + @assert_types 'midi/port', 'num', 'num', 'num', 'num', 'str' + @impulses = { @inputs[1]\unwrap! } - while steps > #@steps - table.insert @steps, 0 - while steps < #@steps - table.remove @steps - - @mask = launch\attach { status: 'control-change', chan: @chan }, (_, _, cc, val) -> @change cc, val - @out + if not @out\unwrap! + @out\set apply_range @inputs[6], 0 - change: (cc, val) => - i = cc - @start - if i < #@steps - @steps[i+1] = val + tick: => + port, i, start, chan, steps = unpack [i\unwrap! for i in *@inputs] + port = @inputs[1]\unwrap! + _, i, start, chan, steps = unpack @inputs - update: (dt) => - launch\tick! - - curr_i = (@i\unwrap 'num') % #@steps + curr_i = i\unwrap! % #@steps + changed = false - @out\set apply_range @range, @steps[curr_i+1] + for msg in port\receive! + if msg.status == 'control-change' and msg.chan == chan + i = msg.a - start + if i < #@steps + @steps[i+1] = msg.b + changed = i == curr_i + if changed or i\dirty! or start\dirty! or chan\dirty! or steps\diry! + @out\set apply_range @inputs[6], @steps[curr_i+1] class gate_seq extends Op - @doc: "(launctl/gate-seq i start chan [steps]) - Gate-Sequencer + @doc: "(launctl/gate-seq port i start chan [steps]) - Gate-Sequencer returns true or false for the i-th step steps (buttons starting from start). steps defaults to 8." diff --git a/lib/osc.moon b/lib/osc.moon index fed1697..3bac3f1 100644 --- a/lib/osc.moon +++ b/lib/osc.moon @@ -2,22 +2,39 @@ import Stream, Op from require 'core' import pack, unpack from require 'osc' import dns, udp from require 'socket' +class addr extends Op + @doc: "(remote host port) - UDP remote definition" + + new: => + super 'udp/remote' + @@udp or= udp! + + setup: (params) => + super params + @assert_types 'str', 'num' + + tick: => + host, port = @unwrap_inputs! + ip = dns.toip host + @out\set { :ip, :port } + class out extends Op @doc: "(out host port path val) - send a value via OSC" new: (...) => - super ... - @@udp or= udp! - setup: (@host, @port, @path, @value) => + setup: (params) => + super params + assert @inputs[3], "need a value" + @assert_types 'udp/remote', 'str', @inputs[3].type update: (dt) => - ip = dns.toip @host\unwrap 'str' - port = @port\unwrap 'num' - msg = pack (@path\unwrap 'str'), @value\unwrap! + remote, path, value = @unwrap_inputs! + msg = pack path, value @@udp\sendto msg, ip, port { + :addr :out } diff --git a/lib/time.moon b/lib/time.moon index 7ce8b84..c49a7d7 100644 --- a/lib/time.moon +++ b/lib/time.moon @@ -1,7 +1,31 @@ -import Stream, Const, Op from require 'core' +import Registry, Value, Op from require 'core' +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 + + setup: (params) => + super params + @last = monotime! + + tick: => + time = monotime! + dt = time - @last + if dt >= 1/60 + @out\set dt + @last = time class lfo extends Op - @doc: "(lfo 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): @@ -10,89 +34,96 @@ wave selects the wave shape from the following (default sin): - tri" tau = math.pi * 2 - new: (...) => - super ... - print "creating LFO" - @out = Stream 'num' + new: => + super 'num' @phase = 0 - default_wave = Const 'str', 'sin' - setup: (@freq, @wave=default_wave) => - assert @freq, "lfo requires a frequency value" - L\trace "setup #{@}, freq=#{@freq}, wave=#{@wave}" - @out + default_wave = Value.str 'sin' + setup: (params) => + super params - update: (dt) => - @phase += dt * @freq\unwrap 'num' - @out\set switch @wave\unwrap! - when 'sin' then .5 + .5 * math.cos @phase * tau - when 'saw' then @phase % 1 - when 'tri' then math.abs (2*@phase % 2) - 1 - else error "unknown wave type" + @inputs[3] or= default_wave + @assert_types 'num', 'num', 'str' + + tick: => + -- if clock is dirty + if @inputs[1].dirty + dt, freq, wave = @unwrap_inputs! + @phase += 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 + else error "unknown wave type" class ramp extends Op - @doc: "(ramp period [max]) - sawtooth lfo + @doc: "(ramp clock period [max]) - sawtooth lfo -ramps from 0 to max (default same as ramp) once every period seconds" +ramps from 0 to max (default same as ramp) once every period seconds." - new: (...) => - super ... - @out = Stream 'num' + new: => + super 'num' @phase = 0 - @out - setup: (@period, @max) => - assert @period, "tick requires a period value" + setup: (params) => + super params + assert @inputs[1].type == 'num', "tick requires a clock value" + assert @inputs[2].type == 'num', "tick requires a period value" - update: (dt) => - period = @period\unwrap 'num' - max = if @max then @max\unwrap! else period - @phase += dt / period + tick: => + -- if clock is dirty + if @inputs[1].dirty + dt, period, max = @unwrap_inputs! + max or= period + @phase += dt / period - if @phase >= 1 - @phase -= 1 + if @phase >= 1 + @phase -= 1 - @out\set @phase * max + @out\set @phase * max class tick extends Op - @doc: "(tick 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 ... - @out = Stream 'num' + new: => + super 'num' @phase = 0 - setup: (@period) => - assert @period, "tick requires a period value" - @out + setup: (params) => + @assert_types 'num', 'num' update: (dt) => - @phase += dt / @period\unwrap 'num' - @out\set math.floor @phase + -- if clock is dirty + if @inputs[1].dirty + dt, period = @unwrap_inputs! + @phase += dt / period + + @out\set math.floor @phase class every extends Op - @doc: "(every period) - sometimes true + @doc: "(every clock period) - trigger every period seconds returns true once every period seconds." - new: (...) => - super ... - @out = Stream 'bool' + new: => + super 'bang' @phase = 0 setup: (@period) => - assert @period, "every requires a period value" - @out + @assert_types 'num', 'num' update: (dt) => - @phase += dt / @period\unwrap 'num' - if @phase > 1 - @phase -= 1 - @out\set true - else - @out\set false + -- if clock is dirty + if @inputs[1].dirty + dt, period = @unwrap_inputs! + @phase += dt / period + + if @phase >= 1 + @phase -= 1 + @out\set true { + :clock :lfo :ramp :tick diff --git a/lib/util.moon b/lib/util.moon index 192c489..04d012f 100644 --- a/lib/util.moon +++ b/lib/util.moon @@ -1,4 +1,4 @@ -import Stream, Const, Op from require 'core' +import Op from require 'core' class switch_ extends Op @doc: "(switch i v0 [v1 v2...]) - switch between multiple inputs @@ -7,85 +7,91 @@ when i is true, the first value is reproduced. when i is false, the second value is reproduced. when i is a num, it is (floor)ed and the matching argument (starting from 0) is reproduced." - setup: (@i, ...) => - @choices = { ... } + setup: (params) => + super params - typ = @choices[1].type - for inp in *@choices[2,] - assert inp.type == typ, "not all values have the same type: #{typ} != #{inp.type}" + { i, first } = @inputs + assert i.type == 'bool' or i.type == 'num', "#{@}: i has to be bool or num" - @out = Stream typ - @out + for inp in *@inputs[3,] + assert inp.type == first.type, "not all values have the same type: #{first.type} != #{inp.type}" + @out = Stream first.type update: (dt) => - i = @i\unwrap! + i = @inputs[1]\unwrap! active = switch i when true - @choices[1] + @inputs[2] when false - @choices[2] + @inputs[3] else - i = 1 + (math.floor i) % #@choices - @choices[i] + i = 2 + (math.floor i) % (#@inputs - 1) + @inputs[i] @out\set active and active\unwrap! -class switch_pause extends Op - @doc: "(switch- i v0 [v1 v2...]) - switch and pause multiple inputs +--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 +-- +-- update: (dt) => +-- 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! -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 - - update: (dt) => - i = @i\unwrap! - active = switch i - when true - @choices[1] - when false - @choices[2] - else - i = 1 + (math.floor i) % #@choices - @choices[i] +class edge extends Op + @doc: "(edge bool) - convert rising edges to bangs" - @out\set if active - active\unwrap! + new: => + super 'bang' -class edge extends Op - setup: (@i) => - @last = false - @out = Stream @i.type - @out + setup: (params) => + super params + @assert_types 'bool' update: (dt) => - now = @i\unwrap! - @out\set not @last and now - @last = now + now = @params[1]\unwrap! + if now and not @last + @out\set true + @last = now class keep extends Op - @doc: "(keep value [default]) - keep the last non-nil value + @doc: "(keep value [init]) - keep the last non-nil value always reproduces the last non-nil value the input produced or default. default defaults to zero." - setup: (@i, @default=Const.num 0) => - @out = Stream @i.type, @default.value - @out + setup: (params) => + super params + { i, init } = @inputs + @out = Value i.type, default and init\unwrap! - update: (dt) => - if next = @i\unwrap! + tick => + if next = @params[1]\unwrap! @out\set next { 'switch': switch_ - 'switch-': switch_pause +-- 'switch-': switch_pause :edge :keep } diff --git a/spec/cell_spec.moon b/spec/cell_spec.moon index b31ef99..fcf9861 100644 --- a/spec/cell_spec.moon +++ b/spec/cell_spec.moon @@ -1,31 +1,31 @@ -import Cell, RootCell, Const, Scope, globals from require 'core' +import Cell, RootCell, Value, Scope, globals from require 'core' import Registry from require 'core.registry' import Logger from require 'logger' Logger.init 'silent' -hello_world = Cell nil, { (Const.sym 'hello'), (Const.str 'world') } -two_plus_two = Cell nil, { (Const.sym '+'), (Const.num 2), (Const.num 2) } +hello_world = Cell nil, { (Value.sym 'hello'), (Value.str 'world') } +two_plus_two = Cell nil, { (Value.sym '+'), (Value.num 2), (Value.num 2) } describe 'Cell', -> it 'supports quoting', -> with hello_world\quote! assert.is.equal Cell, .__class - assert.is.equal (Const.sym 'hello'), \head! - assert.is.same { Const.str 'world' }, \tail! + assert.is.equal (Value.sym 'hello'), \head! + assert.is.same { Value.str 'world' }, \tail! with two_plus_two\quote! assert.is.equal Cell, .__class - assert.is.equal (Const.sym '+'), \head! - assert.is.same { (Const.num 2), (Const.num 2) }, \tail! + assert.is.equal (Value.sym '+'), \head! + assert.is.same { (Value.num 2), (Value.num 2) }, \tail! describe 'RootCell', -> test 'head is always "do"', -> cell = RootCell\parse {} - assert.is.equal (Const.sym 'do'), cell\head! + assert.is.equal (Value.sym 'do'), cell\head! cell = RootCell nil, { hello_world, two_plus_two } - assert.is.equal (Const.sym 'do'), cell\head! + assert.is.equal (Value.sym 'do'), cell\head! test 'tail is all children', -> cell = RootCell\parse {} diff --git a/spec/const_spec.moon b/spec/const_spec.moon deleted file mode 100644 index 4def3a6..0000000 --- a/spec/const_spec.moon +++ /dev/null @@ -1,122 +0,0 @@ -import Const, Op, Action, Scope from require 'core' -import Logger from require 'logger' -Logger.init 'silent' - -class TestOp extends Op - new: (...) => super ... - -class TestAction extends Action - new: (...) => - -describe 'Const', -> - describe 'wraps', -> - test 'numbers', -> - got = Const.wrap 3 - assert.is.equal 'num', got.type - assert.is.equal 3, got.value - - test 'strings', -> - got = Const.wrap "im a happy string" - assert.is.equal 'str', got.type - assert.is.equal "im a happy string", got.value - - test 'Consts', -> - pi = Const 'num', 3.14 - got = Const.wrap pi - - assert.is.equal pi, got - - test 'Opdefs', -> - got = Const.wrap TestOp - - assert.is.equal 'opdef', got.type - assert.is.equal TestOp, got.value - - test 'Bultins', -> - got = Const.wrap TestAction - - assert.is.equal 'builtin', got.type - assert.is.equal TestAction, got.value - - test 'Scopes', -> - sub = Scope! - got = Const.wrap sub - - assert.is.equal 'scope', got.type - assert.is.equal sub, got.value - - test 'tables', -> - pi = Const 'num', 3.14 - got = Const.wrap { :pi } - - assert.is.equal 'scope', got.type - assert.is.equal pi, got.value\get 'pi' - - describe 'unwraps', -> - test 'get!, getc!', -> - assert.is.equal 3.14, (Const.num 3.14)\getc! - assert.is.equal 'hi', (Const.str 'hi')\getc! - assert.is.equal 'hi', (Const.sym 'hi')\getc! - - assert.is.equal 3.14, (Const.num 3.14)\get! - assert.is.equal 'hi', (Const.str 'hi')\get! - assert.is.equal 'hi', (Const.sym 'hi')\get! - - test 'with type assert', -> - assert.is.equal 3.14, (Const.num 3.14)\getc 'num' - assert.is.equal 'hi', (Const.str 'hi')\getc 'str' - assert.is.equal 'hi', (Const.sym 'hi')\getc 'sym' - assert.has_error -> (Const.num 3.14)\getc 'sym' - assert.has_error -> (Const.str 'hi')\getc 'num' - assert.has_error -> (Const.sym 'hi')\getc 'str' - - assert.is.equal 3.14, (Const.num 3.14)\get 'num' - assert.is.equal 'hi', (Const.str 'hi')\get 'str' - assert.is.equal 'hi', (Const.sym 'hi')\get 'sym' - assert.has_error -> (Const.num 3.14)\get 'sym' - assert.has_error -> (Const.str 'hi')\get 'num' - assert.has_error -> (Const.sym 'hi')\get 'str' - - describe 'checks equality', -> - test 'using the type', -> - val = Const 'num', 3 - assert.is.equal (Const.num 3), val - assert.not.equal (Const.str '3'), val - - val = Const 'str', 'hello' - assert.is.equal (Const.str 'hello'), val - assert.not.equal (Const.sym 'hello'), val - - test 'using the value', -> - val = Const 'num', 3 - assert.is.equal (Const.num 3), val - assert.not.equal (Const.num 4), val - - describe 'evaluates literal', -> - test 'constants to themselves', -> - assert_noop = (val) -> assert.is.equal val, val\eval! - - assert_noop Const.num 2 - assert_noop Const.str 'hello' - - test 'symbols in the scope', -> - scope = with Scope! - \set 'number', Const.num 3 - \set 'hello', Const.str "world" - \set 'goodbye', Const.sym "again" - - assert_eval = (sym, val) -> - const = Const.sym sym - assert.is.equal val, const\eval scope - - assert_eval 'number', Const.num 3 - assert_eval 'hello', Const.str "world" - assert_eval 'goodbye', Const.sym "again" - - describe 'quotes literals', -> - test 'as themselves', -> - assert_noop = (val) -> assert.is.equal val, val\quote! - - assert_noop Const.num 2 - assert_noop Const.str 'hello' - assert_noop Const.sym 'world' diff --git a/spec/parsing_spec.moon b/spec/parsing_spec.moon index 2e7da7f..ddf760f 100644 --- a/spec/parsing_spec.moon +++ b/spec/parsing_spec.moon @@ -1,5 +1,5 @@ import space, atom, expr, explist, cell, program, comment from require 'core.parsing' -import Const from require 'core' +import Value from require 'core' import Logger from require 'logger' Logger.init 'silent' @@ -16,48 +16,48 @@ describe 'atom parsing', -> test 'symbols', -> sym = verify_parse_nope atom, 'some-toast nope' assert.is.equal 'sym', sym.type - assert.is.equal 'some-toast', sym\getc! + assert.is.equal 'some-toast', sym\unwrap! assert.is.equal 'some-toast', sym\stringify! describe 'numbers', -> it 'parses ints', -> num = verify_parse_nope atom, '1234 nope' assert.is.equal 'num', num.type - assert.is.equal 1234, num\getc! + assert.is.equal 1234, num\unwrap! assert.is.equal '1234', num\stringify! it 'parses floats', -> num = verify_parse_nope atom, '0.123 nope' assert.is.equal 'num', num.type - assert.is.equal 0.123, num\getc! + assert.is.equal 0.123, num\unwrap! num = verify_parse_nope atom, '.123 nope' assert.is.equal 'num', num.type - assert.is.equal 0.123, num\getc! + assert.is.equal 0.123, num\unwrap! num = verify_parse_nope atom, '0. nope' assert.is.equal 'num', num.type - assert.is.equal 0, num\getc! + assert.is.equal 0, num\unwrap! describe 'strings', -> it 'parses double-quote strings', -> str = verify_parse_nope atom, '"help some stuff!" nope' assert.is.equal 'str', str.type - assert.is.equal 'help some stuff!', str\getc! + assert.is.equal 'help some stuff!', str\unwrap! it 'parses single-quote strings', -> str = verify_parse_nope atom, "'help some stuff!' nope" assert.is.equal 'str', str.type - assert.is.equal "help some stuff!", str\getc! + assert.is.equal "help some stuff!", str\unwrap! it 'handles escapes', -> str = verify_parse_nope atom, '"string with \\"quote\\"s and \\\\" nope' assert.is.equal 'str', str.type - assert.is.equal 'string with \"quote\"s and \\', str\getc! + assert.is.equal 'string with \"quote\"s and \\', str\unwrap! str = verify_parse_nope atom, "'string with \\'quote\\'s and \\\\' nope" assert.is.equal 'str', str.type - assert.is.equal "string with \'quote\'s and \\", str\getc! + assert.is.equal "string with \'quote\'s and \\", str\unwrap! describe 'Cell', -> test 'basic parsing', -> @@ -65,9 +65,9 @@ describe 'Cell', -> "friend" )' assert.is.equal 3, #node.children - assert.is.equal (Const.num 3), node.children[1] - assert.is.equal (Const.sym 'ok-yes'), node.children[2] - assert.is.equal (Const.str 'friend'), node.children[3] + assert.is.equal (Value.num 3), node.children[1] + assert.is.equal (Value.sym 'ok-yes'), node.children[2] + assert.is.equal (Value.str 'friend'), node.children[3] test 'tag parsing', -> node = verify_parse cell, '([42]tagged 2)' @@ -88,8 +88,8 @@ describe 'RootCell parsing', -> node = verify_parse program, str assert.is.equal 2, #node.children - assert.is.equal (Const.num 3), node.children[1] - assert.is.equal (Const.sym 'ok-yes'), node.children[2] + assert.is.equal (Value.num 3), node.children[1] + assert.is.equal (Value.sym 'ok-yes'), node.children[2] it 'at the front of the string', -> verify ' 3\tok-yes' diff --git a/spec/scope_spec.moon b/spec/scope_spec.moon index 9e7ab80..471bbce 100644 --- a/spec/scope_spec.moon +++ b/spec/scope_spec.moon @@ -1,4 +1,4 @@ -import Scope, Const, Op from require 'core' +import Scope, Value, Op from require 'core' import Logger from require 'logger' Logger.init 'silent' @@ -12,27 +12,27 @@ describe 'Scope', -> test 'numbers', -> scope\set_raw 'num', 3 - got = scope\get 'num' + got = (scope\get 'num')\const! assert.is.equal 'num', got.type assert.is.equal 3, got.value test 'strings', -> scope\set_raw 'str', "im a happy string" - got = scope\get 'str' + got = (scope\get 'str')\const! assert.is.equal 'str', got.type assert.is.equal "im a happy string", got.value - test 'Consts', -> - pi = Const 'num', 3.14 + test 'Values', -> + pi = Value 'num', 3.14 scope\set_raw 'pi', pi - assert.is.equal pi, scope\get 'pi' + assert.is.equal pi, (scope\get 'pi')\const! test 'Opdefs', -> scope\set_raw 'test', TestOp - got = scope\get 'test' + got = (scope\get 'test')\const! assert.is.equal 'opdef', got.type assert.is.equal TestOp, got.value @@ -40,22 +40,22 @@ describe 'Scope', -> sub = Scope! scope\set_raw 'sub', sub - got = scope\get 'sub' + got = (scope\get 'sub')\const! assert.is.equal 'scope', got.type assert.is.equal sub, got.value test 'tables', -> - pi = Const 'num', 3.14 + pi = Value 'num', 3.14 scope\set_raw 'math', { :pi } - got = scope\get 'math' + got = (scope\get 'math')\const! assert.is.equal 'scope', got.type assert.is.equal Scope, got.value.__class - assert.is.equal pi, got.value\get 'pi' - assert.is.equal pi, scope\get 'math/pi' + assert.is.equal pi, (got.value\get 'pi')\const! + assert.is.equal pi, (scope\get 'math/pi')\const! - it 'constifies in from_table', -> - pi = Const 'num', 3.14 + it 'wraps Values in from_table', -> + pi = Value 'num', 3.14 scope = Scope.from_table { num: 3 str: "im a happy string" @@ -64,35 +64,35 @@ describe 'Scope', -> test: TestOp } - got = scope\get 'num' + got = (scope\get 'num')\const! assert.is.equal 'num', got.type assert.is.equal 3, got.value - got = scope\get 'str' + got = (scope\get 'str')\const! assert.is.equal 'str', got.type assert.is.equal "im a happy string", got.value - assert.is.equal pi, scope\get 'pi' + assert.is.equal pi, (scope\get 'pi')\const! - got = scope\get 'test' + got = (scope\get 'test')\const! assert.is.equal 'opdef', got.type assert.is.equal TestOp, got.value - got = scope\get 'math' + got = (scope\get 'math')\const! assert.is.equal 'scope', got.type - assert.is.equal pi, scope\get 'math/pi' + assert.is.equal pi, (scope\get 'math/pi')\const! it 'gets from nested scopes', -> root = Scope! a = Scope! b = Scope! - pi = Const 'num', 3.14 + pi = Value 'num', 3.14 b\set_raw 'test', pi a\set_raw 'child', b root\set_raw 'deep', a - assert.is.equal pi, root\get 'deep/child/test' + assert.is.equal pi, (root\get 'deep/child/test')\const! describe 'inheritance', -> root = Scope! @@ -102,13 +102,13 @@ describe 'Scope', -> scope = Scope nil, root it 'allows access', -> - got = scope\get 'inherited' + got = (scope\get 'inherited')\const! assert.is.equal 'str', got.type assert.is.equal "inherited string", got.value it 'can keep defs', -> scope\set_raw 'hidden', "overwritten" - got = scope\get 'hidden' + got = (scope\get 'hidden')\const! assert.is.equal 'str', got.type assert.is.equal "overwritten", got.value diff --git a/spec/value_spec.moon b/spec/value_spec.moon new file mode 100644 index 0000000..bc68c5b --- /dev/null +++ b/spec/value_spec.moon @@ -0,0 +1,112 @@ +import Value, Result, Op, Action, Scope from require 'core' +import Logger from require 'logger' +Logger.init 'silent' + +class TestOp extends Op + new: (...) => super ... + +class TestAction extends Action + new: (...) => + +describe 'Value', -> + describe 'wraps', -> + test 'numbers', -> + got = Value.wrap 3 + assert.is.equal 'num', got.type + assert.is.equal 3, got.value + + test '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', -> + pi = Value 'num', 3.14 + got = Value.wrap pi + + assert.is.equal pi, got + + test 'Opdefs', -> + got = Value.wrap TestOp + + assert.is.equal 'opdef', got.type + assert.is.equal TestOp, got.value + + test 'Bultins', -> + got = Value.wrap TestAction + + assert.is.equal 'builtin', got.type + assert.is.equal TestAction, got.value + + test 'Scopes', -> + sub = Scope! + got = Value.wrap sub + + assert.is.equal 'scope', got.type + assert.is.equal sub, got.value + + test '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!', -> + 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', -> + 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' + assert.has_error -> (Value.num 3.14)\unwrap 'sym' + assert.has_error -> (Value.str 'hi')\unwrap 'num' + assert.has_error -> (Value.sym 'hi')\unwrap 'str' + + describe 'checks equality', -> + test 'using the type', -> + val = Value 'num', 3 + assert.is.equal (Value.num 3), val + assert.not.equal (Value.str '3'), val + + val = Value 'str', 'hello' + assert.is.equal (Value.str 'hello'), val + assert.not.equal (Value.sym 'hello'), val + + test 'using 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', -> + 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', -> + scope = with Scope! + \set 'number', Result value: Value.num 3 + \set 'hello', Result value: Value.str "world" + \set 'goodbye', Result value: Value.sym "again" + + assert_eval = (sym, val) -> + const = Value.sym sym + assert.is.equal val, (const\eval scope)\const! + + assert_eval 'number', Value.num 3 + 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! + + assert_noop Value.num 2 + assert_noop Value.str 'hello' + assert_noop Value.sym 'world' @@ -1,16 +1,9 @@ -([1]import* math time string util) -([2]import osc envelope midi) +([1]import* debug time) +([2]import midi) -([3]defn make-lfo (type) - ([8]fn ([5]f) ([7]lfo ([6]* f 0.5) type))) +([3]trace "help") -([9]def sin-lfo ([10]make-lfo 'sin')) +([4]def device ([5]midi/input "system:midi_capture_2")) -([11]defn send (name value) - ([13]osc/out '127.0.0.1' 9000 - ([12].. '/param/' name '/set') - value)) - -([23]send 'radius' ([25]([24]envelope/ar ([14]midi/cc 0) ([15]midi/cc 1)) ([27]midi/gate 36))) - -([19]send 'offset' ([20]sin-lfo ([16]keep ([26]midi/cc 24 0 4)))) +([6]out 'name' ([7]midi/cc device 0)) +([8]out 'gate' ([9]midi/gate device 44)) |
