aboutsummaryrefslogtreecommitdiffstats
path: root/spec/internal
diff options
context:
space:
mode:
authors-ol <s+removethis@s-ol.nu>2022-02-05 10:11:13 +0000
committers-ol <s+removethis@s-ol.nu>2025-03-02 14:24:49 +0000
commitc484cce5ab1f6acd14ecad30adb586a6d5fa0fd1 (patch)
treea45c702a83e150fea5b14b279e5f0cab9b65bc3e /spec/internal
parentexamples: more smoothing in love example :) (diff)
downloadalive-c484cce5ab1f6acd14ecad30adb586a6d5fa0fd1.tar.gz
alive-c484cce5ab1f6acd14ecad30adb586a6d5fa0fd1.zip
rearrange spec, fix for Lua 5.1
Diffstat (limited to 'spec/internal')
-rw-r--r--spec/internal/cell_spec.moon58
-rw-r--r--spec/internal/input_spec.moon156
-rw-r--r--spec/internal/match_spec.moon307
-rw-r--r--spec/internal/parsing_spec.moon141
-rw-r--r--spec/internal/pureop_spec.moon128
-rw-r--r--spec/internal/registry_spec.moon8
-rw-r--r--spec/internal/result/const_spec.moon158
-rw-r--r--spec/internal/result/evt_spec.moon144
-rw-r--r--spec/internal/result/sig_spec.moon116
-rw-r--r--spec/internal/rtnode_spec.moon216
-rw-r--r--spec/internal/scope_spec.moon166
-rw-r--r--spec/internal/tag_spec.moon91
-rw-r--r--spec/internal/type_spec.moon175
13 files changed, 1864 insertions, 0 deletions
diff --git a/spec/internal/cell_spec.moon b/spec/internal/cell_spec.moon
new file mode 100644
index 0000000..a8e79fe
--- /dev/null
+++ b/spec/internal/cell_spec.moon
@@ -0,0 +1,58 @@
+import do_setup from require 'spec.test_setup'
+import Cell, RootCell from require 'alv.cell'
+import Constant, Scope, Tag, SimpleRegistry, globals from require 'alv'
+
+setup do_setup
+
+hello_world = Cell.parse Tag.parse('2'), {
+ '', (Constant.sym 'hello'), ' ', (Constant.str 'world'), ''
+}
+two_plus_two = Cell.parse Tag.parse('3'), {
+ '', (Constant.sym '+'), ' ', (Constant.num 2), ' ', (Constant.num 2), ''
+}
+
+describe 'Cell', ->
+ describe 'when cloned', ->
+ parent = Tag.blank '1'
+ with hello_world\clone parent
+ it 'keeps children', ->
+ assert.is.equal Cell, .__class
+ assert.is.equal (Constant.sym 'hello'), \head!
+ assert.is.same { Constant.str 'world' }, \tail!
+
+ it 'clones the tag', ->
+ assert.is.equal hello_world.tag, .tag.original
+ assert.is.equal parent, .tag.parent
+
+ describe 'when evaluated', ->
+ it 'errors when empty', ->
+ cell = Cell.parse {''}
+ assert.has.error -> cell\eval globals
+
+ it 'evaluates its head', ->
+ head = Constant.sym 'trace'
+ cell = Cell.parse { '', head, ' ', (Constant.sym 'true'), '' }
+
+ s = spy.on head, 'eval'
+ cell\eval globals
+ assert.spy(s).was_called_with (match.is_ref head), (match.is_ref globals)
+
+describe 'RootCell', ->
+ test 'tag is always [0]', ->
+ cell = Cell.parse_root {}
+ assert.is.equal '[0]', cell.tag\stringify!
+
+ test 'head is always "do"', ->
+ cell = Cell.parse_root {}
+ assert.is.equal (Constant.sym 'do'), cell\head!
+
+ cell = RootCell nil, { hello_world, two_plus_two }
+ assert.is.equal (Constant.sym 'do'), cell\head!
+
+ test 'tail is all children', ->
+ cell = Cell.parse_root {}
+ assert.is.same {}, cell\tail!
+
+ cell = RootCell nil, { hello_world, two_plus_two }
+ assert.is.same { hello_world, two_plus_two },
+ cell\tail!
diff --git a/spec/internal/input_spec.moon b/spec/internal/input_spec.moon
new file mode 100644
index 0000000..04cf16e
--- /dev/null
+++ b/spec/internal/input_spec.moon
@@ -0,0 +1,156 @@
+import do_setup, do_teardown from require 'spec.test_setup'
+import Input, T, Result from require 'alv.base'
+
+setup do_setup
+teardown do_teardown
+
+basic_tests = (result, input) ->
+ it 'gives access to the Result', ->
+ assert.is.equal result, input.result
+
+ it 'forwards :unwrap', ->
+ assert.is.equal result\unwrap!, input\unwrap!, nil
+ assert.is.equal result\unwrap!, input!, nil
+
+ it 'gives access to the type string', ->
+ assert.is.equal result.type, input\type!
+
+ it 'gives access to the metatype string', ->
+ assert.is.equal result.metatype, input\metatype!
+
+describe 'Input.cold', ->
+ result = T.num\mk_sig 1
+ input = Input.cold result
+
+ basic_tests result, input
+
+ it 'is marked cold', ->
+ assert.is.equal 'cold', input.mode
+
+ it 'is never dirty', ->
+ assert.is.false input\dirty!
+ result\set 2
+ assert.is.false input\dirty!
+
+ input\setup nil
+ assert.is.false input\dirty!
+ input\finish_setup!
+
+ new_input = Input.cold T.num\mk_sig 3
+ new_input\setup input
+ assert.is.false new_input\dirty!
+ new_input.result\set 4
+ assert.is.false new_input\dirty!
+ input\finish_setup!
+
+describe 'Input.hot', ->
+ describe 'with Constant', ->
+ result = T.num\mk_const 1
+ input = Input.hot result
+
+ basic_tests result, input
+
+ it 'is marked cold', ->
+ assert.is.equal 'cold', input.mode
+
+ describe 'at evaltime', ->
+ it 'is dirty when new', ->
+ assert.is.false result\dirty!
+
+ newinput = Input.hot result
+ newinput\setup nil
+ assert.is.true newinput\dirty!
+ newinput\finish_setup!
+
+ it 'is dirty when different', ->
+ newval = T.num\mk_const 2
+
+ assert.is.false newval\dirty!
+ newinput = Input.hot newval
+ newinput\setup input
+ assert.is.true newinput\dirty!
+ newinput\finish_setup!
+
+ it 'is not dirty when equal', ->
+ newval = T.num\mk_const 1
+
+ assert.is.false newval\dirty!
+ newinput = Input.hot newval
+ newinput\setup input
+ assert.is.false newinput\dirty!
+ newinput\finish_setup!
+
+ describe 'with EvtStream', ->
+ result = T.num\mk_evt!
+ input = Input.hot result
+
+ basic_tests result, input
+
+ it 'is marked hot', ->
+ assert.is.equal 'hot', input.mode
+
+ it 'is dirty when the EvtStream is dirty', ->
+ assert.is.false input\dirty!
+ assert.is.false result\dirty!
+
+ input\setup nil
+ assert.is.false input\dirty!
+ input\finish_setup!
+
+ COPILOT\next_tick!
+ result\set 1
+
+ assert.is.true input\dirty!
+ assert.is.true result\dirty!
+
+ input\setup nil
+ assert.is.true input\dirty!
+ input\finish_setup!
+
+ assert.is.true input\dirty!
+ assert.is.true result\dirty!
+
+ describe 'with SigStream', ->
+ result = T.num\mk_sig 1
+ local input
+
+ describe 'at evaltime', ->
+ it 'is dirty when new', ->
+ assert.is.false result\dirty!
+
+ input = Input.hot result
+ input\setup nil
+ assert.is.true input\dirty!
+ input\finish_setup!
+
+ it 'is dirty when different', ->
+ newval = T.num\mk_sig 2
+
+ assert.is.false newval\dirty!
+ newinput = Input.hot newval
+ newinput\setup input
+ assert.is.true newinput\dirty!
+ newinput\finish_setup!
+
+ it 'is not dirty when equal', ->
+ newval = T.num\mk_sig!
+ newval\set 1
+
+ assert.is.true newval\dirty!
+ newinput = Input.hot newval
+ newinput\setup input
+ assert.is.false newinput\dirty!
+ newinput\finish_setup!
+
+ it 'is marked hot', ->
+ assert.is.equal 'hot', input.mode
+
+ describe 'at runtime', ->
+ it 'is dirty when the result is dirty', ->
+ result\set 3
+ assert.is.true result\dirty!
+ assert.is.true input\dirty!
+
+ COPILOT\next_tick!
+ assert.is.false result\dirty!
+ assert.is.false input\dirty!
diff --git a/spec/internal/match_spec.moon b/spec/internal/match_spec.moon
new file mode 100644
index 0000000..b9d5ef6
--- /dev/null
+++ b/spec/internal/match_spec.moon
@@ -0,0 +1,307 @@
+import const, sig, evt, any from require 'alv.base.match'
+import Op, Input from require 'alv.base'
+import RTNode, T, Error from require 'alv'
+
+op_with_inputs = (inputs) ->
+ with Op!
+ \setup inputs if inputs
+
+mk_const = (type, const) ->
+ result = T[type]\mk_const true
+ RTNode :result, op: op_with_inputs { Input.hot result }
+
+mk_val = (type, const) ->
+ result = T[type]\mk_sig true
+ RTNode :result, op: op_with_inputs { Input.hot result }
+
+mk_evt = (type, const) ->
+ result = T[type]\mk_evt!
+ RTNode :result, op: op_with_inputs { Input.hot result }
+
+describe 'sig and evt', ->
+ describe 'type-less shorthand', ->
+ it 'matches metatype', ->
+ str = mk_val 'str'
+ num = mk_val 'num'
+ assert.is.equal str, sig!\match { str }
+ assert.is.equal num, sig!\match { num }
+ assert.has.error -> const!\match { str }
+ assert.has.error -> const!\match { num }
+ assert.has.error -> evt!\match { str }
+ assert.has.error -> evt!\match { num }
+
+ str = mk_evt 'str'
+ num = mk_evt 'num'
+ assert.has.error -> sig!\match { str }
+ assert.has.error -> sig!\match { num }
+ assert.has.error -> const!\match { str }
+ assert.has.error -> const!\match { num }
+ assert.is.equal str, evt!\match { str }
+ assert.is.equal num, evt!\match { num }
+
+ str = mk_const 'str'
+ num = mk_const 'num'
+ assert.is.equal str, sig!\match { str }
+ assert.is.equal num, sig!\match { num }
+ assert.is.equal str, const!\match { str }
+ assert.is.equal num, const!\match { num }
+ assert.has.error -> evt!\match { str }
+ assert.has.error -> evt!\match { num }
+
+ it 'can recall the type', ->
+ value = sig!!
+ event = evt!!
+ two_equal_values = value + value
+ two_equal_events = event + event
+
+ str1 = mk_val 'str'
+ str2 = mk_val 'str'
+ num = mk_val 'num'
+ assert.is.same { str1, str2 }, two_equal_values\match { str1, str2 }
+ assert.is.same { str2, str1 }, two_equal_values\match { str2, str1 }
+ assert.is.same { num, num }, two_equal_values\match { num, num }
+ assert.has.error -> two_equal_values\match { str1, num }
+ assert.has.error -> two_equal_values\match { num, str2 }
+ assert.has.error -> two_equal_events\match { str1, str2 }
+
+ str1 = mk_evt 'str'
+ str2 = mk_evt 'str'
+ num = mk_evt 'num'
+ assert.is.same { str1, str2 }, two_equal_events\match { str1, str2 }
+ assert.is.same { str2, str1 }, two_equal_events\match { str2, str1 }
+ assert.is.same { num, num }, two_equal_events\match { num, num }
+ assert.has.error -> two_equal_events\match { str1, num }
+ assert.has.error -> two_equal_events\match { num, str2 }
+ assert.has.error -> two_equal_values\match { str1, str2 }
+
+ it 'stringifies well', ->
+ assert.is.equal 'any=', tostring const!
+ assert.is.equal 'any!', tostring evt!
+ assert.is.equal 'any~', tostring sig!
+
+ describe 'typed shorthand', ->
+ it 'matches by metatype', ->
+ str = mk_val 'str'
+ num = mk_val 'num'
+ assert.is.equal str, sig.str\match { str }
+ assert.is.equal num, sig.num\match { num }
+ assert.has.error -> const.str\match { str }
+ assert.has.error -> const.num\match { num }
+ assert.has.error -> evt.str\match { str }
+ assert.has.error -> evt.num\match { num }
+
+ str = mk_evt 'str'
+ num = mk_evt 'num'
+ assert.has.error -> sig.str\match { str }
+ assert.has.error -> sig.num\match { num }
+ assert.has.error -> const.str\match { str }
+ assert.has.error -> const.num\match { num }
+ assert.is.equal str, evt.str\match { str }
+ assert.is.equal num, evt.num\match { num }
+
+ str = mk_const 'str'
+ num = mk_const 'num'
+ assert.is.equal str, sig.str\match { str }
+ assert.is.equal num, sig.num\match { num }
+ assert.is.equal str, const.str\match { str }
+ assert.is.equal num, const.num\match { num }
+ assert.has.error -> evt.str\match { str }
+ assert.has.error -> evt.num\match { num }
+
+ it 'matches by type', ->
+ str = mk_const 'str'
+ num = mk_const 'num'
+ assert.is.equal str, sig.str\match { str }
+ assert.is.equal num, sig.num\match { num }
+ assert.is.equal str, const.str\match { str }
+ assert.is.equal num, const.num\match { num }
+ assert.has.error -> sig.num\match { str }
+ assert.has.error -> sig.str\match { num }
+
+ str = mk_val 'str'
+ num = mk_val 'num'
+ assert.is.equal str, sig.str\match { str }
+ assert.is.equal num, sig.num\match { num }
+ assert.has.error -> const.num\match { str }
+ assert.has.error -> const.str\match { num }
+ assert.has.error -> sig.num\match { str }
+ assert.has.error -> sig.str\match { num }
+
+ str = mk_evt 'str'
+ num = mk_evt 'num'
+ assert.is.equal str, evt.str\match { str }
+ assert.is.equal num, evt.num\match { num }
+ assert.has.error -> const.num\match { str }
+ assert.has.error -> const.str\match { num }
+ assert.has.error -> evt.num\match { str }
+ assert.has.error -> evt.str\match { num }
+
+ it 'stringifies well', ->
+ assert.is.equal 'str!', tostring evt.str
+ assert.is.equal 'num!', tostring evt.num
+ assert.is.equal 'str~', tostring sig.str
+ assert.is.equal 'num~', tostring sig.num
+ assert.is.equal 'str=', tostring const.str
+ assert.is.equal 'num=', tostring const.num
+
+describe 'choice', ->
+ str = mk_val 'str'
+ num = mk_val 'num'
+ bool = mk_val 'bool'
+ choice = sig.str / sig.num
+
+ it 'matches either type', ->
+ assert.is.equal str, choice\match { str }
+ assert.is.equal num, choice\match { num }
+ assert.has.error -> choice\match { bool }
+
+ it 'can recall the choice', ->
+ same = choice!
+ assert.is.equal num, same\match { num }
+
+ same = same + same
+ assert.is.same { str, str }, same\match { str, str }
+ assert.is.same { num, num }, same\match { num, num }
+ assert.has.error -> same\match { str, num }
+ assert.has.error -> same\match { num, str }
+ assert.has.error -> same\match { bool, bool }
+
+ it 'makes inner types recall', ->
+ same = any!!
+ same = same + same
+ assert.is.same { str, str }, same\match { str, str }
+ assert.is.same { num, num }, same\match { num, num }
+ assert.is.same { bool, bool }, same\match { bool, bool }
+ assert.has.error -> same\match { str, num }
+ assert.has.error -> same\match { num, str }
+
+ it 'stringifies well', ->
+ assert.is.equal '(str~ | num~)', tostring choice
+
+describe 'sequence', ->
+ str = mk_val 'str'
+ num = mk_val 'num'
+ bool = mk_evt 'bool'
+ seq = sig.str + sig.num + evt.bool
+
+ it 'matches all types in order', ->
+ assert.is.same { str, num, bool }, seq\match { str, num, bool }
+
+ it 'can assign non-numeric keys', ->
+ named = seq\named 'str', 'num', 'bool'
+ assert.is.same { :str, :num, :bool }, named\match { str, num, bool }
+ assert.is.same { str, num, bool }, seq\match { str, num, bool }
+
+ it 'fails if too little arguments', ->
+ assert.has.error -> seq\match { str, num }
+
+ it 'fails if too many arguments', ->
+ assert.has.error -> seq\match { str, num, bool, bool }
+
+ it 'can handle optional children', ->
+ opt = -sig.str + sig.num
+ assert.is.same { str, num }, opt\match { str, num }
+ assert.is.same { nil, num }, opt\match { num }
+ assert.has.error -> opt\match { str, str, num }
+ assert.has.error -> opt\match { str, num, num }
+
+ it 'can handle repeat children', ->
+ rep = sig.str + sig.num*2
+ assert.is.same { str, {num} }, rep\match { str, num }
+ assert.is.same { str, {num,num} }, rep\match { str, num, num }
+ assert.has.error -> rep\match { str }
+ assert.has.error -> rep\match { str, num, num, num }
+
+ it 'stringifies well', ->
+ assert.is.equal '(str~ num~ bool!)', tostring seq
+
+describe 'repeat', ->
+ str = mk_val 'str'
+ num = mk_val 'num'
+
+ times = (n, arg) -> return for i=1,n do arg
+
+ it '*x is [1,x[', ->
+ rep = sig.str*3
+ assert.has.error -> rep\match (times 0, str)
+ assert.is.same (times 1, str), rep\match (times 1, str)
+ assert.is.same (times 2, str), rep\match (times 2, str)
+ assert.is.same (times 3, str), rep\match (times 3, str)
+ assert.has.error -> rep\match (times 4, str)
+ assert.has.error -> rep\match (times 3, num)
+
+ it '*0 is [1,[', ->
+ rep = sig.str*0
+ assert.has.error -> rep\match (times 0, str)
+ assert.is.same (times 1, str), rep\match (times 1, str)
+ assert.is.same (times 2, str), rep\match (times 2, str)
+ assert.is.same (times 20, str), rep\match (times 20, str)
+ assert.has.error -> rep\match (times 3, num)
+
+ it '^x is [0,x[', ->
+ rep = sig.str^3
+ assert.is.same {}, rep\match {}
+ assert.is.same (times 1, str), rep\match (times 1, str)
+ assert.is.same (times 2, str), rep\match (times 2, str)
+ assert.is.same (times 3, str), rep\match (times 3, str)
+ assert.has.error -> rep\match (times 4, str)
+ assert.has.error -> rep\match (times 3, num)
+
+ it '^0 is [0,[', ->
+ rep = sig.str^0
+ assert.is.same {}, rep\match {}
+ assert.is.same (times 1, str), rep\match (times 1, str)
+ assert.is.same (times 2, str), rep\match (times 2, str)
+ assert.is.same (times 20, str), rep\match (times 20, str)
+ assert.has.error -> rep\match (times 3, num)
+
+ it ':rep(min, max) does anything else', ->
+ rep = sig.str\rep 2, 2
+ assert.has.error -> rep\match {}
+ assert.has.error -> rep\match (times 1, str)
+ assert.is.same (times 2, str), rep\match (times 2, str)
+ assert.has.error -> rep\match (times 3, str)
+ assert.has.error -> rep\match (times 2, num)
+
+ it 'works with complex inner types', ->
+ rep = (sig.num + sig.str)\rep 2, 2
+ assert.has.error -> rep\match {}
+ assert.has.error -> rep\match {num, str}
+ assert.has.error -> rep\match {num, num}
+ assert.is.same {{num, str}, {num, str}}, rep\match {num, str, num, str}
+ assert.has.error -> rep\match {str, str, str, str}
+ assert.has.error -> rep\match {num, str, num, str, num, str}
+
+ it 'stringifies well', ->
+ assert.is.equal 'str~{1-3}', tostring sig.str*3
+ assert.is.equal 'str~{1-*}', tostring sig.str*0
+ assert.is.equal 'str~{0-*}', tostring sig.str^0
+ assert.is.equal 'str~{2-2}', tostring sig.str\rep 2, 2
+
+describe 'complex nesting', ->
+ bang = mk_evt 'bang'
+ str = mk_val 'str'
+ num = mk_val 'num'
+ num_c = mk_const 'num'
+ pattern = -evt.bang + sig.num*4 +
+ (sig.str + (sig.num / sig.str))\named('key', 'val')^0
+
+ it 'just works', ->
+ assert.is.same { bang, { num, num }, {} }, pattern\match { bang, num, num }
+ assert.is.same { nil, { num }, { { key: str, val: num },
+ { key: str, val: str } } },
+ pattern\match { num, str, num, str, str }
+ assert.has.error -> pattern\match { num, str }
+ assert.has.error -> pattern\match { num_c, str }
+ assert.has.error -> pattern\match { bang, num_c, num, num_c, num, num, num }
+ assert.has.error -> pattern\match { bang, bang, num }
+ assert.has.error -> pattern\match { num, str, num_c, str }
+ assert.has.error -> pattern\match { num, str, num, str, mk_val 'bool' }
+
+ it 'stringifies well', ->
+ assert.is.equal '(bang!? num~{1-4} (str~ (num~ | str~)){0-*})', tostring pattern
+
+ it 'gives useful error feedback', ->
+ msg = "couldn't match arguments (num~ str~ bool~) against pattern #{pattern}"
+ err = Error 'argument', msg
+ assert.has.error (-> pattern\match { num, str, mk_val 'bool' }), err
diff --git a/spec/internal/parsing_spec.moon b/spec/internal/parsing_spec.moon
new file mode 100644
index 0000000..0486276
--- /dev/null
+++ b/spec/internal/parsing_spec.moon
@@ -0,0 +1,141 @@
+import space, atom, expr, explist, cell, program, comment
+ from require 'alv.parsing'
+import Constant from require 'alv'
+import Logger from require 'alv.logger'
+Logger\init 'silent'
+
+verify_parse = (parser, val) ->
+ with assert parser\match val
+ assert.is.equal val, \stringify!
+
+verify_parse_nope = (parser, val) ->
+ with assert parser\match val
+ without_nope = val\match '^(.*) nope$'
+ assert.is.equal without_nope, \stringify!
+
+describe 'atom parsing', ->
+ test 'symbols', ->
+ sym = verify_parse_nope atom, 'some-toast nope'
+ assert.is.equal (Constant.sym 'some-toast'), sym
+
+ describe 'numbers', ->
+ it 'parses ints', ->
+ num = verify_parse_nope atom, '1234 nope'
+ assert.is.equal (Constant.num 1234), num
+
+ it 'parses floats', ->
+ num = verify_parse_nope atom, '0.123 nope'
+ assert.is.equal (Constant.num 0.123), num
+
+ num = verify_parse_nope atom, '.123 nope'
+ assert.is.equal (Constant.num 0.123), num
+
+ num = verify_parse_nope atom, '0. nope'
+ assert.is.equal (Constant.num 0), num
+
+ describe 'strings', ->
+ it 'parses double-quote strings', ->
+ str = verify_parse_nope atom, '"help some stuff!" nope'
+ assert.is.equal (Constant.str 'help some stuff!'), str
+
+ it 'parses single-quote strings', ->
+ str = verify_parse_nope atom, "'help some stuff!' nope"
+ assert.is.equal (Constant.str "help some stuff!"), str
+
+ it 'handles escapes', ->
+ str = verify_parse_nope atom, '"string with \\"quote\\"s and \\\\" nope'
+ assert.is.equal (Constant.str 'string with \"quote\"s and \\'), str
+
+ str = verify_parse_nope atom, "'string with \\'quote\\'s and \\\\' nope"
+ assert.is.equal (Constant.str "string with \'quote\'s and \\"), str
+
+describe 'Cell', ->
+ test 'basic parsing', ->
+ node = verify_parse cell, '( 3 ok-yes
+ "friend" )'
+
+ assert.is.equal 3, #node.children
+ assert.is.equal (Constant.num 3), node.children[1]
+ assert.is.equal (Constant.sym 'ok-yes'), node.children[2]
+ assert.is.equal (Constant.str 'friend'), node.children[3]
+
+ test 'tag parsing', ->
+ node = verify_parse cell, '([42]tagged 2)'
+
+ assert.is.equal 2, #node.children
+ assert.is.equal 42, node.tag.value
+
+ test 'tag parsing with whitespace', ->
+ node = verify_parse cell, '([42]
+ tagged 2)'
+
+ assert.is.equal 2, #node.children
+ assert.is.equal 42, node.tag.value
+
+describe 'RootCell parsing', ->
+ describe 'handles whitespace', ->
+ verify = (str) ->
+ node = verify_parse program, str
+
+ assert.is.equal 2, #node.children
+ assert.is.equal (Constant.num 3), node.children[1]
+ assert.is.equal (Constant.sym 'ok-yes'), node.children[2]
+
+ it 'at the front of the string', ->
+ verify ' 3\tok-yes'
+
+ it 'at the end of the string', ->
+ verify ' 3\tok-yes\n'
+
+ it 'everywhere', ->
+ verify ' 3\tok-yes\n'
+
+test 'whitespace', ->
+ assert.is.equal ' ', space\match ' '
+ assert.is.equal '\n\t ', space\match '\n\t '
+
+describe 'comments', ->
+ comment = comment / 1
+ it 'are parsed', ->
+ str = '#(this is a comment)'
+ assert.is.equal str, comment\match str
+
+ it 'extend to matching braces', ->
+ str = '#(this is a comment #(with nested comments))'
+ assert.is.equal str, comment\match str
+
+ it 'can nest', ->
+ str = '#(this is a comment (with nested parenthesis))'
+ assert.is.equal str, comment\match str
+
+describe 'resynthesis', ->
+ test 'mixed parsing', ->
+ str = '( 3 ok-yes
+ "friend" )'
+ node = verify_parse program, str
+ assert.is.equal str, node\stringify!
+
+ test 'complex', ->
+ str = '
+ #(send a CC controlled LFO to /radius)
+ (osc "/radius" (lfo (cc 14)))
+
+ (osc rot
+ (step
+ #(whenever a kick is received...)
+ (note "kick")
+
+ #(..cycle through random rotation values)
+ (random-rot)
+ (random-rot)
+ (random-rot)
+ (random-rot)
+ )
+ ) '
+ matched = assert.is.truthy verify_parse program, str
+ assert.is.equal str, matched\stringify!
+
+ test 'nested tags', ->
+ str = '([2]a ([3]b))'
+ matched = assert.is.truthy verify_parse program, str
+ assert.is.equal str, matched\stringify!
diff --git a/spec/internal/pureop_spec.moon b/spec/internal/pureop_spec.moon
new file mode 100644
index 0000000..a8fa823
--- /dev/null
+++ b/spec/internal/pureop_spec.moon
@@ -0,0 +1,128 @@
+import do_setup, do_teardown, invoke_op from require 'spec.test_setup'
+import PureOp, Input, T, sig, any from require 'alv.base'
+import RTNode from require 'alv'
+
+setup do_setup
+teardown do_teardown
+
+class TestPureOp extends PureOp
+ pattern: (any.num / sig.str)*3
+ type: T.num
+ tick: => @out\set 1
+
+literal = (result) ->
+ eval: ->
+ si = T.num\mk_sig!
+ RTNode :result, side_inputs: { [si]: Input.hot si }
+
+describe 'PureOp', ->
+ it 'matches the pattern', ->
+ assert.has.error -> invoke_op TestPureOp, {}
+ assert.has.error -> invoke_op TestPureOp, { literal T.bool\mk_evt! }
+ invoke_op TestPureOp, { literal T.num\mk_const 1 }
+ invoke_op TestPureOp, { literal T.str\mk_const 'hello' }
+ invoke_op TestPureOp, { literal T.num\mk_evt! }
+
+ describe 'with constant inputs', ->
+ local rtn
+ it 'ticks once', ->
+ tiq = spy.on TestPureOp.__base, 'tick'
+ rtn = invoke_op TestPureOp, { T.num\mk_const(1), T.str\mk_const('hello') }
+ assert.spy(tiq).was_called_with match.is_ref(rtn.op), true
+ assert.is.equal 1, rtn.result!
+
+ it 'is constant', ->
+ assert.is.equal '=', rtn\metatype!
+
+ describe 'with signal inputs', ->
+ a = T.num\mk_sig 1
+ b = T.num\mk_sig 2
+ c = T.num\mk_sig 3
+ tiq = spy.on TestPureOp.__base, 'tick'
+ rtn = invoke_op TestPureOp, { (literal a), (literal b), (literal c) }
+ op = rtn.op
+
+ it 'sets up hot inputs', ->
+ assert.is.equal 3, #op.inputs
+ assert.is.equal a, op.inputs[1].result
+ assert.is.equal b, op.inputs[2].result
+ assert.is.equal c, op.inputs[3].result
+ assert.is.equal 'hot', op.inputs[1].mode
+ assert.is.equal 'hot', op.inputs[2].mode
+ assert.is.equal 'hot', op.inputs[3].mode
+ assert.spy(tiq).was_called!
+
+ it 'has signal output', ->
+ assert.is.equal '~', op.out.metatype
+
+ describe 'with event inputs', ->
+ a = T.num\mk_sig 1
+ b = T.num\mk_evt 2
+ c = T.num\mk_sig 3
+ tiq = spy.on TestPureOp.__base, 'tick'
+ rtn = invoke_op TestPureOp, { (literal a), (literal b), (literal c) }
+ op = rtn.op
+
+ it 'sets up hot input only for evt', ->
+ assert.is.equal 3, #op.inputs
+ assert.is.equal a, op.inputs[1].result
+ assert.is.equal b, op.inputs[2].result
+ assert.is.equal c, op.inputs[3].result
+ assert.is.equal 'cold', op.inputs[1].mode
+ assert.is.equal 'hot', op.inputs[2].mode
+ assert.is.equal 'cold', op.inputs[3].mode
+ assert.spy(tiq).was_not_called!
+
+ it 'has event output', ->
+ assert.is.equal '!', op.out.metatype
+
+ it 'only allows one event input', ->
+ a, b = T.num\mk_evt!, T.num\mk_evt!
+ assert.has.error -> invoke_op TestPureOp, { (literal a), (literal b) }
+
+ it 'supports nested input patterns', ->
+ class NestedInputOp extends PureOp
+ pattern: (any.num + sig.str)\named('a', 'b')\rep 2, 2
+ type: T.num
+ tick: => @out\set 1
+
+ num = T.num\mk_sig 1
+ str = T.str\mk_sig 'hello'
+ oth = T.num\mk_evt 2
+ args = { (literal num), (literal str), (literal oth), (literal str) }
+ rtn = invoke_op NestedInputOp, args
+ op = rtn.op
+
+ assert.is.equal 2, #op.inputs
+ assert.is.equal num, op.inputs[1].a.result
+ assert.is.equal str, op.inputs[1].b.result
+ assert.is.equal oth, op.inputs[2].a.result
+ assert.is.equal str, op.inputs[2].b.result
+ assert.is.equal 'cold', op.inputs[1].a.mode
+ assert.is.equal 'cold', op.inputs[1].b.mode
+ assert.is.equal 'hot', op.inputs[2].a.mode
+ assert.is.equal 'cold', op.inputs[2].b.mode
+
+ it 'supports dynamically generating the output type', ->
+ class DynamicOp extends PureOp
+ pattern: sig.num + any!
+ type: (inputs) => inputs[2]\type!
+ tick: => @out\set @inputs[2]!
+ typ = spy.on DynamicOp, 'type'
+
+ a = T.num\mk_sig 1
+ num = T.num\mk_sig 1
+ str = T.str\mk_sig 1
+ sym = T.sym\mk_evt 1
+
+ rtn = invoke_op DynamicOp, { (literal a), (literal num) }
+ assert.is.equal '~', rtn.result.metatype
+ assert.is.equal num, rtn.result
+
+ rtn = invoke_op DynamicOp, { (literal a), (literal str) }
+ assert.is.equal '~', rtn.result.metatype
+ assert.is.equal str, rtn.result
+
+ rtn = invoke_op DynamicOp, { (literal a), (literal sym) }
+ assert.is.equal '!', rtn.result.metatype
+ assert.is.equal T.sym, rtn.result.type
diff --git a/spec/internal/registry_spec.moon b/spec/internal/registry_spec.moon
new file mode 100644
index 0000000..4b0cfc3
--- /dev/null
+++ b/spec/internal/registry_spec.moon
@@ -0,0 +1,8 @@
+import Registry, Tag from require 'alv.registry'
+import Logger from require 'alv.logger'
+Logger\init 'silent'
+
+mk = ->
+ mock destroy: =>
+
+describe 'registry', ->
diff --git a/spec/internal/result/const_spec.moon b/spec/internal/result/const_spec.moon
new file mode 100644
index 0000000..3f31306
--- /dev/null
+++ b/spec/internal/result/const_spec.moon
@@ -0,0 +1,158 @@
+import do_setup from require 'spec.test_setup'
+import Constant, RTNode, Scope, SimpleRegistry, T from require 'alv'
+import Op, Builtin from require 'alv.base'
+
+class TestOp extends Op
+ new: (...) => super ...
+
+class TestBuiltin extends Builtin
+ new: (...) =>
+
+setup do_setup
+
+describe 'Constant', ->
+ it 'requires a value', ->
+ assert.has.error -> Constant.num!
+ assert.has.error -> Constant T.num
+ assert.has.no.error -> Constant T.bool, false
+
+ it 'stringifies well', ->
+ assert.is.equal "<num= 4>", tostring Constant.num 4
+ assert.is.equal "<bool= true>", tostring Constant.bool true
+ assert.is.equal "<bool= false>", tostring Constant.bool false
+
+ describe '.wrap', ->
+ it 'wraps numbers', ->
+ got = Constant.wrap 3
+ assert.is.equal T.num, got.type
+ assert.is.equal 3, got.value
+
+ it 'wraps strings', ->
+ got = Constant.wrap "im a happy string"
+ assert.is.equal T.str, got.type
+ assert.is.equal "im a happy string", got.value
+
+ it 'wraps Constants', ->
+ pi = Constant.num 3.14
+ got = Constant.wrap pi
+
+ assert.is.equal pi, got
+
+ it 'wraps Opdefs', ->
+ got = Constant.wrap TestOp
+
+ assert.is.equal T.opdef, got.type
+ assert.is.equal TestOp, got.value
+
+ it 'wraps Bultins', ->
+ got = Constant.wrap TestBuiltin
+
+ assert.is.equal T.builtin, got.type
+ assert.is.equal TestBuiltin, got.value
+
+ it 'wraps Scopes', ->
+ sub = Scope!
+ got = Constant.wrap sub
+
+ assert.is.equal T.scope, got.type
+ assert.is.equal sub, got.value
+
+ it 'wraps tables', ->
+ pi = Constant.num 3.14
+ got = Constant.wrap { :pi }
+
+ assert.is.equal T.scope, got.type
+ assert.is.equal pi, (got.value\get 'pi')\const!
+
+ describe ':unwrap', ->
+ it 'returns the raw value!', ->
+ assert.is.equal 3.14, (Constant.num 3.14)\unwrap!
+ assert.is.equal 'hi', (Constant.str 'hi')\unwrap!
+ assert.is.equal 'hi', (Constant.sym 'hi')\unwrap!
+
+ test 'can assert the type', ->
+ assert.is.equal 3.14, (Constant.num 3.14)\unwrap T.num
+ assert.is.equal 'hi', (Constant.str 'hi')\unwrap T.str
+ assert.is.equal 'hi', (Constant.sym 'hi')\unwrap T.sym
+ assert.has_error -> (Constant.num 3.14)\unwrap T.sym
+ assert.has_error -> (Constant.str 'hi')\unwrap T.num
+ assert.has_error -> (Constant.sym 'hi')\unwrap T.str
+
+ test 'has __call shorthand', ->
+ assert.is.equal 3.14, (Constant.num 3.14)!
+ assert.is.equal 'hi', (Constant.str 'hi')!
+ assert.is.equal 'hi', (Constant.sym 'hi')!
+ assert.is.equal 3.14, (Constant.num 3.14) T.num
+ assert.is.equal 'hi', (Constant.str 'hi') T.str
+ assert.is.equal 'hi', (Constant.sym 'hi') T.sym
+ assert.has_error -> (Constant.num 3.14) T.sym
+ assert.has_error -> (Constant.str 'hi') T.num
+ assert.has_error -> (Constant.sym 'hi') T.str
+
+ describe 'overrides __eq', ->
+ it 'compares the type', ->
+ val = Constant.num 3
+ assert.is.equal (Constant.num 3), val
+ assert.not.equal (Constant.str '3'), val
+
+ val = Constant.str 'hello'
+ assert.is.equal (Constant.str 'hello'), val
+ assert.not.equal (Constant.sym 'hello'), val
+
+ it 'compares the value', ->
+ val = Constant.num 3
+ assert.is.equal (Constant.num 3), val
+ assert.not.equal (Constant.num 4), val
+
+ it ':dirty is always false', ->
+ val = Constant.num 3
+ assert.is.false val\dirty!
+
+ val.value = 4
+ assert.is.false val\dirty!
+
+ describe ':eval', ->
+ it 'turns numbers into consts', ->
+ assert_noop = (val) ->
+ assert.is.equal val, val\eval!\const!
+
+ assert_noop Constant.num 2
+ assert_noop Constant.str 'hello'
+
+ it 'looks up symbols in the scope', ->
+ scope = with Scope!
+ \set 'number', RTNode result: Constant.num 3
+ \set 'hello', RTNode result: Constant.str "world"
+ \set 'goodbye', RTNode result: Constant.sym "again"
+
+ assert_eval = (sym, val) ->
+ const = Constant.sym sym
+ assert.is.equal val, (const\eval scope)\const!
+
+ assert_eval 'number', Constant.num 3
+ assert_eval 'hello', Constant.str "world"
+ assert_eval 'goodbye', Constant.sym "again"
+
+ it ':clones literals as themselves', ->
+ assert_noop = (val) -> assert.is.equal val, val\clone!
+
+ assert_noop Constant.num 2
+ assert_noop Constant.str 'hello'
+ assert_noop Constant.sym 'world'
+
+ describe ':fork', ->
+ it 'is equal to the original', ->
+ a = Constant.num 2
+ b = Constant.str 'asdf'
+ c = Constant T.weird, {}, '(raw)'
+
+ aa, bb, cc = a\fork!, b\fork!, c\fork!
+ assert.is.equal a, aa
+ assert.is.equal b, bb
+ assert.is.equal c, cc
+
+ assert.is.false aa\dirty!
+ assert.is.false bb\dirty!
+ assert.is.false cc\dirty!
+
+ assert.is.equal c.raw, cc.raw
diff --git a/spec/internal/result/evt_spec.moon b/spec/internal/result/evt_spec.moon
new file mode 100644
index 0000000..46d4cec
--- /dev/null
+++ b/spec/internal/result/evt_spec.moon
@@ -0,0 +1,144 @@
+import do_setup from require 'spec.test_setup'
+import EvtStream, RTNode, Scope, SimpleRegistry, T from require 'alv'
+import Op, Builtin from require 'alv.base'
+
+setup do_setup
+
+describe 'EvtStream', ->
+ it 'stringifies well', ->
+ number = EvtStream T.num
+ assert.is.equal "<num! nil>", tostring number
+
+ number\set 4
+ assert.is.equal "<num! 4>", tostring number
+
+ bool = EvtStream T.bool
+ bool\set true
+ assert.is.equal "<bool! true>", tostring bool
+
+ COPILOT\next_tick!
+
+ bool\set false
+ assert.is.equal "<bool! false>", tostring bool
+
+ describe ':unwrap', ->
+ it 'returns the set value', ->
+ stream = EvtStream T.num
+ assert.is.nil stream\unwrap!
+
+ stream\set 3.14
+ assert.is.equal 3.14, stream\unwrap!
+
+ it 'returns nil if not dirty', ->
+ stream = EvtStream T.num
+ assert.is.nil stream\unwrap!
+
+ stream\set 3.14
+ COPILOT\next_tick!
+ assert.is.nil stream\unwrap!
+
+ test 'can assert the type', ->
+ assert.is.nil (EvtStream T.num)\unwrap T.num
+ assert.is.nil (EvtStream T.str)\unwrap T.str
+ assert.is.nil (EvtStream T.sym)\unwrap T.sym
+ assert.has_error -> (EvtStream T.num)\unwrap T.sym
+ assert.has_error -> (EvtStream T.str)\unwrap T.num
+ assert.has_error -> (EvtStream T.sym)\unwrap T.str
+
+ test 'has __call shorthand', ->
+ stream = EvtStream T.num
+ assert.is.nil stream!
+
+ stream\set 3.14
+ assert.is.equal 3.14, stream!
+
+ describe ':set', ->
+ it 'sets the value', ->
+ stream = EvtStream T.num
+ assert.is.false stream\dirty!
+
+ stream\set 4
+ assert.is.equal 4, stream\unwrap!
+ assert.is.true stream\dirty!
+
+ COPILOT\next_tick!
+
+ assert.is.false stream\dirty!
+ stream\set 3
+ assert.is.equal 3, stream\unwrap!
+ assert.is.true stream\dirty!
+
+ it 'ignores nil values', ->
+ stream = EvtStream T.num
+ assert.is.nil stream\unwrap!
+ assert.is.false stream\dirty!
+
+ stream\set!
+ assert.is.nil stream\unwrap!
+ assert.is.false stream\dirty!
+
+ stream\set nil
+ assert.is.nil stream\unwrap!
+ assert.is.false stream\dirty!
+
+ stream\set false
+ assert.is.equal false, stream\unwrap!
+ assert.is.true stream\dirty!
+
+ it 'errors when set twice', ->
+ stream = EvtStream T.num
+ stream\set 1
+ assert.has.error -> stream\set 2
+ assert.is.equal 1, stream\unwrap!
+
+ it 'resets on the next tick', ->
+ stream = EvtStream T.num
+ stream\set 1
+
+ COPILOT\next_tick!
+
+ assert.is.false stream\dirty!
+ stream\set 2
+ assert.is.equal 2, stream\unwrap!
+ assert.is.true stream\dirty!
+
+ describe ':fork', ->
+ it 'is clean', ->
+ a = EvtStream T.num
+ b = EvtStream T.str
+ b\set 'asdf'
+
+ aa, bb = a\fork!, b\fork!
+ assert.is.nil aa!
+ assert.is.nil bb!
+ assert.is.false aa\dirty!
+ assert.is.false bb\dirty!
+
+ it 'leaves the original', ->
+ a = EvtStream T.num
+ b = EvtStream T.str
+ b\set 'asdf'
+
+ aa, bb = a\fork!, b\fork!
+ assert.is.nil a!
+ assert.is.equal 'asdf', b!
+ assert.is.false a\dirty!
+ assert.is.true b\dirty!
+
+ it 'isolates the original from the fork', ->
+ a = EvtStream T.num
+ b = EvtStream T.str
+
+ aa, bb = a\fork!, b\fork!
+ a\set 1
+ bb\set 2
+
+ assert.is.equal 1, a!
+ assert.is.true a\dirty!
+ assert.is.nil aa!
+ assert.is.false aa\dirty!
+
+ assert.is.equal 2, bb!
+ assert.is.true bb\dirty!
+ assert.is.nil b!
+ assert.is.false b\dirty!
diff --git a/spec/internal/result/sig_spec.moon b/spec/internal/result/sig_spec.moon
new file mode 100644
index 0000000..65363bc
--- /dev/null
+++ b/spec/internal/result/sig_spec.moon
@@ -0,0 +1,116 @@
+import do_setup from require 'spec.test_setup'
+import SigStream, Constant, RTNode, Scope, SimpleRegistry, T from require 'alv'
+import Op, Builtin from require 'alv.base'
+
+setup do_setup
+
+describe 'SigStream', ->
+ it 'stringifies well', ->
+ assert.is.equal "<num~ 4>", tostring SigStream T.num, 4
+ assert.is.equal "<bool~ true>", tostring SigStream T.bool, true
+ assert.is.equal "<bool~ false>", tostring SigStream T.bool, false
+
+ describe ':unwrap', ->
+ it 'returns the raw value!', ->
+ assert.is.equal 3.14, (SigStream T.num, 3.14)\unwrap!
+ assert.is.equal 'hi', (SigStream T.str, 'hi')\unwrap!
+ assert.is.equal 'hi', (SigStream T.sym, 'hi')\unwrap!
+
+ test 'can assert the type', ->
+ assert.is.equal 3.14, (SigStream T.num, 3.14)\unwrap T.num
+ assert.is.equal 'hi', (SigStream T.str, 'hi')\unwrap T.str
+ assert.is.equal 'hi', (SigStream T.sym, 'hi')\unwrap T.sym
+ assert.has_error -> (SigStream T.num, 3.14)\unwrap T.sym
+ assert.has_error -> (SigStream T.str, 'hi')\unwrap T.num
+ assert.has_error -> (SigStream T.sym, 'hi')\unwrap T.str
+
+ test 'has __call shorthand', ->
+ assert.is.equal 3.14, (SigStream T.num, 3.14)!
+ assert.is.equal 'hi', (SigStream T.str, 'hi')!
+ assert.is.equal 'hi', (SigStream T.sym, 'hi')!
+ assert.is.equal 3.14, (SigStream T.num, 3.14) T.num
+ assert.is.equal 'hi', (SigStream T.str, 'hi') T.str
+ assert.is.equal 'hi', (SigStream T.sym, 'hi') T.sym
+ assert.has_error -> (SigStream T.num, 3.14) T.sym
+ assert.has_error -> (SigStream T.str, 'hi') T.num
+ assert.has_error -> (SigStream T.sym, 'hi') T.str
+
+ describe 'overrides __eq', ->
+ it 'compares the type', ->
+ val = SigStream T.num, 3
+ assert.is.equal (SigStream T.num, 3), val
+ assert.not.equal (SigStream T.str, '3'), val
+
+ val = SigStream T.str, 'hello'
+ assert.is.equal (SigStream T.str, 'hello'), val
+ assert.not.equal (SigStream T.sym, 'hello'), val
+
+ it 'compares the value', ->
+ val = SigStream T.num, 3
+ assert.is.equal (SigStream T.num, 3), val
+ assert.not.equal (SigStream T.num, 4), val
+
+ it 'can be compared to a Constant', ->
+ val = SigStream T.num, 3
+ assert.is.equal (Constant.num 3), val
+ assert.not.equal (Constant.num 4), val
+
+ val = SigStream T.str, 'hello'
+ assert.is.equal (Constant.str 'hello'), val
+ assert.not.equal (Constant.sym 'hello'), val
+
+ describe ':set', ->
+ it 'sets the value', ->
+ val = SigStream T.num, 3
+ assert.is.equal (SigStream T.num, 3), val
+
+ val\set 4
+ assert.is.equal (SigStream T.num, 4), val
+ assert.not.equal (SigStream T.num, 3), val
+
+ it 'marks the value dirty', ->
+ val = SigStream T.num, 3
+ assert.is.false val\dirty!
+
+ val\set 4
+ assert.is.true val\dirty!
+
+ describe ':fork', ->
+ it 'is equal to the original', ->
+ a = SigStream T.num, 2
+ b = SigStream T.str, 'asdf'
+ c = with SigStream T.weird, {}, '(raw)'
+ \set {}
+
+ aa, bb, cc = a\fork!, b\fork!, c\fork!
+ assert.is.equal a, aa
+ assert.is.equal b, bb
+ assert.is.equal c, cc
+
+ assert.is.false aa\dirty!
+ assert.is.false bb\dirty!
+ assert.is.true cc\dirty!
+
+ assert.is.equal c.raw, cc.raw
+
+ it 'isolates the original from the fork', ->
+ a = SigStream T.num, 3
+ b = with SigStream T.weird, {}, '(raw)'
+ \set {}
+
+ aa, bb = a\fork!, b\fork!
+
+ bb\set {false}
+
+ assert.is.same {}, b!
+ assert.is.same {false}, bb!
+ assert.is.true b\dirty!
+ assert.is.true bb\dirty!
+
+ COPILOT\next_tick!
+ aa\set 4
+
+ assert.is.equal 3, a!
+ assert.is.equal 4, aa!
+ assert.is.false a\dirty!
+ assert.is.true aa\dirty!
diff --git a/spec/internal/rtnode_spec.moon b/spec/internal/rtnode_spec.moon
new file mode 100644
index 0000000..f757067
--- /dev/null
+++ b/spec/internal/rtnode_spec.moon
@@ -0,0 +1,216 @@
+import do_setup from require 'spec.test_setup'
+import RTNode, Scope, SimpleRegistry from require 'alv'
+import T, Input, Op, Constant from require 'alv.base'
+
+class IOOp extends Op
+ new: (...) =>
+ super ...
+ @is_dirty = true
+
+ poll: => @is_dirty
+
+make_op = (Class, inputs={}) ->
+ with Class!
+ \setup inputs
+
+setup do_setup
+
+describe 'RTNode', ->
+ describe 'constructor', ->
+ it 'takes result, children', ->
+ result = Constant.num 3
+
+ a = RTNode!
+ b = RTNode!
+ children = { a, b }
+
+ node = RTNode :result, :children
+
+ assert.is.equal result, node.result
+ assert.is.same children, node.children
+
+ it 'takes op and detects io_ops', ->
+ op = make_op Op
+ rtn = RTNode :op
+ assert.is.equal op, rtn.op
+ assert.is.same {}, rtn.io_ops
+
+ op = make_op IOOp
+ rtn = RTNode :op
+ assert.is.equal op, rtn.op
+ assert.is.same { op }, rtn.io_ops
+
+ describe 'detects Op inputs', ->
+ it '(hot -> side_input)', ->
+ sig = T.num\mk_sig!
+ inp = Input.hot sig
+ rtn = RTNode op: make_op Op, { inp }
+ assert.is.same { [sig]: inp }, rtn.side_inputs
+
+ it '(cold -> discard)', ->
+ rtn = RTNode op: make_op Op, { Input.cold T.num\mk_sig! }
+ assert.is.same {}, rtn.side_inputs
+
+ it "(childrens' results -> discard)", ->
+ child = RTNode op: make_op(Op), result: T.num\mk_sig 123
+ parent = RTNode children: { child }, op: make_op Op, { Input.hot child.result }
+ assert.is.same {}, parent.side_inputs
+
+ describe 'lifts up', ->
+ it 'side_inputs from children', ->
+ expected_inputs = {}
+ children = {}
+
+ for i=1,3
+ result = T.num\mk_sig!
+ input = Input.hot result
+ table.insert children, RTNode op: make_op Op, { input }
+ expected_inputs[result] = input
+
+ mid_rtn = RTNode children: children
+ out_rtn = RTNode children: { mid_rtn }
+
+ assert.is.same expected_inputs, mid_rtn.side_inputs
+ assert.is.same expected_inputs, out_rtn.side_inputs
+
+ it 'io_ops from children', ->
+ a_op = make_op IOOp
+ a_rtn = RTNode op: a_op
+
+ b_op = make_op IOOp
+ b_rtn = RTNode op: b_op
+
+ c_op = make_op Op
+ c_rtn = RTNode op: c_op
+
+ mid_rtn = RTNode children: { a_rtn, b_rtn, c_rtn }
+ out_op = make_op IOOp
+ out_rtn = RTNode children: { mid_rtn }, op: out_op
+
+ assert.is.same { a_op, b_op }, mid_rtn.io_ops
+ assert.is.same { a_op, b_op, out_op }, out_rtn.io_ops
+
+ it ':type gets type and assets value', ->
+ node = RTNode result: Constant.num 2
+ assert.is.equal T.num, node\type!
+
+ node = RTNode!
+ assert.has.error -> node\type!
+
+ it ':is_const', ->
+ result = Constant.num 2
+ pure = RTNode :result
+ impure = RTNode op: make_op Op, { Input.hot T.num\mk_sig! }
+
+ assert.is.true pure\is_const!
+ assert.is.false impure\is_const!
+
+ assert.is.equal result, pure\const!
+ assert.has.error -> impure\const!
+ assert.has.error (-> impure\const 'test'), 'test'
+
+ it ':make_ref', ->
+ result = T.num\mk_sig 2
+ input = Input.hot result
+ op = make_op IOOp, { input }
+ thick = RTNode :result, :op, children: { RTNode!, RTNode! }
+ ref = thick\make_ref!
+
+ assert ref
+ assert.is.equal thick.result, ref.result
+ assert.is.same thick.side_inputs, ref.side_inputs
+ assert.is.same {}, ref.children
+ assert.is.same {}, ref.io_ops
+ assert.is.nil ref.op
+
+ describe ':poll_io', ->
+ it 'polls all io_ops in tree', ->
+ child = RTNode op: make_op IOOp
+ node = RTNode children: { child }, op: make_op IOOp
+
+ sc = spy.on child.op, 'poll'
+ sn = spy.on node.op, 'poll'
+ node\poll_io!
+
+ assert.spy(sc).was_called_with match.ref child.op
+ assert.spy(sn).was_called_with match.ref node.op
+
+ it 'returns whether any ops were dirty', ->
+ child = RTNode op: make_op IOOp
+ node = RTNode children: { child }, op: make_op IOOp
+
+ assert.is.true node\poll_io!
+
+ child.op.is_dirty = false
+ assert.is.true node\poll_io!
+
+ node.op.is_dirty = false
+ assert.is.false node\poll_io!
+
+ child.op.is_dirty = true
+ assert.is.true node\poll_io!
+
+ describe ':tick', ->
+ local a_value, a_child, a_input
+ local b_value, b_child, b_input
+ before_each ->
+ a_value = T.num\mk_evt!
+ a_input = Input.hot a_value
+ a_child = RTNode op: make_op Op, { a_input }
+
+ b_value = T.num\mk_evt!
+ b_input = Input.hot b_value
+ b_child = RTNode op: make_op Op, { b_input }
+
+ it 'updates children when a side_input is dirty', ->
+ a_value\set 1
+ assert.is.true a_input\dirty!
+ assert.is.false b_input\dirty!
+
+ a = spy.on a_child, 'tick'
+ b = spy.on b_child, 'tick'
+
+ node = RTNode children: { a_child, b_child }
+ node\tick!
+
+ assert.spy(a).was_called_with match.ref a_child
+ assert.spy(b).was_called_with match.ref b_child
+
+ it 'early-outs when no side_inputs are dirty', ->
+ assert.is.false a_input\dirty!
+ assert.is.false b_input\dirty!
+
+ a = spy.on a_child, 'tick'
+ b = spy.on b_child, 'tick'
+
+ node = RTNode children: { a_child, b_child }
+ node\tick!
+
+ assert.spy(a).was_not_called!
+ assert.spy(b).was_not_called!
+
+ it 'updates op when any op-inputs are dirty', ->
+ a_value\set 1
+ assert.is.true a_input\dirty!
+ assert.is.false b_input\dirty!
+
+ op = make_op Op, { a: Input.hot a_value }
+ s = spy.on op, 'tick'
+
+ node = RTNode :op, children: { a_child, b_child }
+ node\tick!
+
+ assert.spy(s).was_called_with match.ref op
+
+ it 'early-outs when no op-inputs are dirty', ->
+ a_value\set 1
+ assert.is.true a_input\dirty!
+ assert.is.false b_input\dirty!
+
+ op = make_op Op, { Input.hot b_value }
+ s = spy.on op, 'tick'
+
+ node = RTNode :op, children: { a_child, b_child }
+ node\tick!
+
+ assert.spy(s).was_not_called!
diff --git a/spec/internal/scope_spec.moon b/spec/internal/scope_spec.moon
new file mode 100644
index 0000000..b4a55f1
--- /dev/null
+++ b/spec/internal/scope_spec.moon
@@ -0,0 +1,166 @@
+import Scope, T, Constant, RTNode from require 'alv'
+import Op from require 'alv.base'
+import Logger from require 'alv.logger'
+Logger\init 'silent'
+
+class TestOp extends Op
+ new: (...) => super ...
+
+wrap_res = (result) -> RTNode :result
+
+describe 'Scope', ->
+ describe 'constifies', ->
+ scope = Scope!
+
+ test 'numbers', ->
+ scope\set_raw 'num', 3
+
+ got = (scope\get 'num')\const!
+ assert.is.equal (Constant.num 3), got
+
+ test 'strings', ->
+ scope\set_raw 'str', "im a happy string"
+
+ got = (scope\get 'str')\const!
+ assert.is.equal (Constant.str "im a happy string"), got
+
+ test 'Values', ->
+ pi = Constant.num 3.14
+ scope\set_raw 'pi', pi
+ assert.is.equal pi, (scope\get 'pi')\const!
+
+ test 'Opdefs', ->
+ scope\set_raw 'test', TestOp
+
+ got = (scope\get 'test')\const!
+ assert.is.equal TestOp, got T.opdef
+
+ test 'Scopes', ->
+ sub = Scope!
+ scope\set_raw 'sub', sub
+
+ got = (scope\get 'sub')\const!
+ assert.is.equal sub, got T.scope
+
+ test 'tables', ->
+ pi = Constant.num 3.14
+ scope\set_raw 'math', { :pi }
+
+ got = (scope\get 'math')\const!
+ assert.is.equal T.scope, got.type
+ assert.is.equal Scope, got.value.__class
+ assert.is.equal pi, (got.value\get 'pi')\const!
+ assert.is.equal pi, (scope\get 'math/pi')\const!
+
+ it 'stringifies well', ->
+ scope = Scope.from_table {
+ num: 3
+ str: "im a happy string"
+ :pi
+ math: :pi
+ test: TestOp
+ }
+ sub = Scope Scope scope
+
+ assert.is.equal "<Scope ^1 []>", tostring sub
+ assert.is.equal "<Scope ^-1 [math, num, str, test]>", tostring scope
+ scope\set_raw 'a', Constant.num 1
+ scope\set_raw 'b', Constant.num 2
+ scope\set_raw 'c', Constant.num 3
+ assert.is.equal "<Scope ^-1 [a, b, c, math, num, …]>", tostring scope
+
+ it 'wraps Values in from_table', ->
+ pi = Constant.num 3.14
+ scope = Scope.from_table {
+ num: 3
+ str: "im a happy string"
+ :pi
+ math: :pi
+ test: TestOp
+ }
+
+ got = (scope\get 'num')\const!
+ assert.is.equal 3, got T.num
+
+ got = (scope\get 'str')\const!
+ assert.is.equal "im a happy string", got T.str
+
+ assert.is.equal pi, (scope\get 'pi')\const!
+
+ got = (scope\get 'test')\const!
+ assert.is.equal TestOp, got T.opdef
+
+ got = (scope\get 'math')\const!
+ assert.is.equal T.scope, got.type
+ assert.is.equal pi, (scope\get 'math/pi')\const!
+
+ it 'gets from nested scopes', ->
+ root = Scope!
+ a = Scope!
+ b = Scope!
+
+ pi = Constant.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')\const!
+
+ describe 'can set symbols', ->
+ one = wrap_res Constant.num 1
+ two = wrap_res Constant.num 2
+ scope = Scope!
+
+ it 'disallows re-setting symbols', ->
+ scope\set 'test', one
+ assert.is.equal one, scope\get 'test'
+
+ it 'throws if overwriting', ->
+ assert.has.error -> scope\set 'test', two
+ assert.is.equal one, scope\get 'test'
+
+ describe 'inheritance', ->
+ root = Scope!
+ root\set_raw 'hidden', 1234
+ root\set_raw 'inherited', "inherited string"
+
+ scope = Scope root
+
+ it 'allows access', ->
+ got = (scope\get 'inherited')\const!
+ assert.is.equal "inherited string", got T.str
+
+ it 'can be shadowed', ->
+ scope\set_raw 'hidden', "overwritten"
+
+ got = (scope\get 'hidden')\const!
+ assert.is.equal "overwritten", got T.str
+
+ describe 'dynamic inheritance', ->
+ root = Scope!
+ dyn_root = Scope!
+
+ root\set_raw 'normal', 'normal'
+ root\set_raw '*dynamic*', 'normal'
+ dyn_root\set_raw 'normal', 'dynamic'
+ dyn_root\set_raw '*dynamic*', 'dynamic'
+
+ dyn_root\set_raw '*nested*', { value: 3 }
+
+ it 'follows a different parent', ->
+ merged = Scope root, dyn_root
+ assert.is.equal 'normal', (merged\get 'normal').result!
+ assert.is.equal 'dynamic', (merged\get '*dynamic*').result!
+
+ it 'falls back to the immediate parent', ->
+ merged = Scope root
+ assert.is.equal 'normal', (merged\get '*dynamic*').result!
+
+ it 'looks in self first', ->
+ merged = Scope root
+ merged\set_raw '*dynamic*', 'merged'
+ assert.is.equal 'merged', (merged\get '*dynamic*').result!
+
+ it 'can resolve nested', ->
+ merged = Scope root, dyn_root
+ assert.is.equal 3, (merged\get '*nested*/value').result!
diff --git a/spec/internal/tag_spec.moon b/spec/internal/tag_spec.moon
new file mode 100644
index 0000000..f92dca0
--- /dev/null
+++ b/spec/internal/tag_spec.moon
@@ -0,0 +1,91 @@
+import do_setup from require 'spec.test_setup'
+import Tag from require 'alv.tag'
+import Registry from require 'alv.registry'
+
+setup do_setup
+
+describe 'Tag', ->
+ describe 'should be constructable', ->
+ it 'by parsing', ->
+ tag = Tag.parse '2'
+ assert tag
+ assert.is.equal 2, tag.value
+ assert.is.equal '[2]', tag\stringify!
+ assert.is.equal '2', tostring tag
+
+ it 'as blank Tags', ->
+ tag = Tag.blank!
+ assert tag
+ assert.is.nil tag.value
+ assert.is.equal '', tag\stringify!
+ assert.is.equal '?', tostring tag
+
+ describe 'should be clonable', ->
+ do_asserts = (tag, expect) ->
+ assert tag
+ assert.is.nil tag.value
+ assert.is.equal expect, tostring tag
+ assert.has.error tag\stringify
+
+ it 'from parsed tags', ->
+ parent = Tag.parse '1'
+ original = Tag.parse '2'
+ tag = original\clone parent
+ do_asserts tag, '1.2'
+
+ it 'but not from blank tags', ->
+ parent = Tag.parse '1'
+ original = Tag.blank!
+ tag = original\clone parent
+ do_asserts tag, '1.?'
+
+ it 'with blank parent', ->
+ parent = Tag.blank!
+ original = Tag.parse '2'
+ tag = original\clone parent
+ do_asserts tag, '?.2'
+
+ it 'completely blank', ->
+ parent = Tag.blank!
+ original = Tag.blank!
+ tag = original\clone parent
+ do_asserts tag, '?.?'
+
+ describe 'should be set-able', ->
+ it 'only if blank', ->
+ tag = Tag.parse '42'
+ assert.has.error -> tag\set 43
+
+ clone = tag\clone Tag.parse '3'
+ assert.has.error -> clone\set 42
+
+ clone = tag\clone Tag.blank!
+ assert.has.error -> clone\set 42
+
+ it 'and stores the value', ->
+ blank = Tag.blank!
+ blank\set 12
+
+ assert.is.equal blank.value, 12
+
+ it 'sets the original if cloned', ->
+ original = Tag.blank!
+ parent = Tag.parse '7'
+
+ o_set = spy.on original, 'set'
+ p_set = spy.on parent, 'set'
+
+ clone = original\clone parent
+ clone\set 11
+
+ assert.spy(o_set).was_called_with (match.is_ref original), 11
+ assert.spy(p_set).was_not_called!
+
+ assert.is.equal original.value, 11
+
+ it 'requires the parent to be registered if cloned', ->
+ original = Tag.blank!
+ parent = Tag.blank!
+
+ clone = original\clone parent
+ assert.has.error -> clone\set 11
diff --git a/spec/internal/type_spec.moon b/spec/internal/type_spec.moon
new file mode 100644
index 0000000..727e90b
--- /dev/null
+++ b/spec/internal/type_spec.moon
@@ -0,0 +1,175 @@
+require 'spec.test_setup'
+import T, Type, Primitive, Array, Struct from require 'alv.type'
+import ancestor from require 'alv.util'
+
+bool = Primitive 'bool'
+num = Primitive 'num'
+str = Primitive 'str'
+
+describe 'Primitive', ->
+ it 'inherits from Type', ->
+ assert.is.equal Type, ancestor Primitive.__class
+
+ it 'exposes name', ->
+ assert.is.equal 'bool', bool.name
+ assert.is.equal 'num', num.name
+ assert.is.equal 'str', str.name
+
+ it 'stringifies well', ->
+ assert.is.equal 'bool', tostring bool
+ assert.is.equal 'num', tostring num
+ assert.is.equal 'str', tostring str
+
+ it 'implements __eq sensibly', ->
+ assert.is.equal (Primitive 'bool'), bool
+ assert.is.equal (Primitive 'num'), num
+ assert.is.equal (Primitive 'str'), str
+ assert.not.equal num, bool
+ assert.not.equal str, num
+
+ it ':pp pretty-prints values', ->
+ assert.is.equal 'true', bool\pp true
+ assert.is.equal 'false', bool\pp false
+ assert.is.equal 'nil', bool\pp nil
+ assert.is.equal '4.134', num\pp 4.134
+ assert.is.equal '"hello"', str\pp "hello"
+
+ it ':eq compares values', ->
+ a, b = {}, {}
+ assert.is.true bool\eq true, true
+ assert.is.false bool\eq true, false
+ assert.is.true num\eq 1, 1
+ assert.is.false num\eq 1, -1
+ assert.is.true num\eq a, a
+ assert.is.false num\eq a, b
+
+ it ':get errors', ->
+ assert.has.error -> bool\get 'hello'
+ assert.has.error -> bool\get 2
+ assert.has.error -> bool\get!
+
+describe 'Array', ->
+ vec3 = Array 3, num
+ str32 = Array 2, Array 3, str
+
+ it 'inherits from Type', ->
+ assert.is.equal Type, ancestor Array.__class
+
+ it 'exposes size', ->
+ assert.is.equal 3, vec3.size
+ assert.is.equal 2, str32.size
+
+ it 'exposes inner type', ->
+ assert.is.equal num, vec3.type
+ assert.is.equal (Array 3, str), str32.type
+
+ it 'stringifies well', ->
+ assert.is.equal 'num[3]', tostring vec3
+ assert.is.equal 'my-type[3][24]', tostring Array 24, Array 3, 'my-type'
+
+ it 'implements __eq sensibly', ->
+ assert.is.equal vec3, Array 3, num
+ assert.not.equal vec3, Array 2, num
+ assert.not.equal vec3, Array 3, str
+
+ it ':pp pretty-prints values', ->
+ assert.is.equal '[1 2 3]', vec3\pp { 1, 2, 3 }
+ assert.is.equal '[["a" "b" "c"] ["d" "e" "f"]]',
+ str32\pp { {'a', 'b', 'c'}, {'d', 'e', 'f'} }
+
+ it ':eq compares values', ->
+ a, b, c = {1, 2, 3}, {1, 2, 3}, {}
+ assert.is.true vec3\eq a, a
+ assert.is.true vec3\eq a, b
+ assert.is.false vec3\eq a, {1, 2, 4}
+ assert.is.true vec3\eq {1, 2, c}, {1, 2, c}
+ assert.is.false vec3\eq {1, 2, c}, {1, 2, {}}
+ assert.is.true str32\eq { {'a', 'b', 'c'}, {'d', 'e', 'f'} },
+ { {'a', 'b', 'c'}, {'d', 'e', 'f'} }
+ assert.is.false str32\eq { {'a', 'b', 'c'}, {'d', 'e', 'f'} },
+ { {'a', 'b', 'c'}, {'d', 'e', 'g'} }
+
+ it ':get verifies size range', ->
+ assert.has.error -> vec3\get -1
+ assert.is.equal num, vec3\get 0
+ assert.is.equal num, vec3\get 1
+ assert.is.equal num, vec3\get 2
+ assert.has.error -> vec3\get 3
+
+ assert.is.equal (Array 3, str), str32\get 1
+
+ assert.has.error -> vec3\get!
+ assert.has.error -> vec3\get 'fail'
+
+describe 'Struct', ->
+ play = Struct { note: str, dur: num }
+ abc = Struct { c: num, b: num, a: num }
+
+ it 'inherits from Type', ->
+ assert.is.equal Type, ancestor Struct.__class
+
+ it 'exposes inner types', ->
+ assert.is.same { note: str, dur: num }, play.types
+ assert.is.same { c: num, b: num, a: num }, abc.types
+
+ it 'stringifies well', ->
+ assert.is.equal '{dur: num note: str}', tostring play
+ assert.is.equal '{a: num b: num c: num}', tostring abc
+
+ it 'implements __eq sensibly', ->
+ assert.is.equal play, Struct { note: str, dur: num }
+ assert.not.equal play, Struct { note: str }
+ assert.not.equal play, Struct { note: str, dur: str }
+ assert.not.equal play, Struct { note: str, dur: num, extra: num }
+
+ it ':pp pretty-prints values', ->
+ assert.is.equal '{dur: 0.5 note: "a"}', play\pp { dur: 0.5, note: 'a' }
+ assert.is.equal '{a: 1 b: 2 c: 3}', abc\pp { a: 1, b: 2, c: 3 }
+
+ it ':eq compares values', ->
+ a, b, c = { dur: 0.5, note: 'a' }, { dur: 0.5, note: 'a' }, {}
+ assert.is.true play\eq a, a
+ assert.is.true play\eq a, b
+ assert.is.false play\eq a, { dur: 0.5, note: 'b' }
+ assert.is.true play\eq { dur: 0.5, note: c }, { dur: 0.5, note: c }
+ assert.is.false play\eq { dur: 0.5, note: c }, { dur: 0.5, note: {} }
+
+ it ':get verifies key exists', ->
+ assert.is.equal str, play\get 'note'
+ assert.is.equal num, play\get 'dur'
+ assert.has.error -> play\get!
+ assert.has.error -> play\get ''
+ assert.has.error -> play\get 'something'
+
+describe 'T', ->
+ it 'provides shorthand for Primitives', ->
+ assert.is.equal num, T.num
+ assert.is.equal str, T.str
+ assert.is.equal (Primitive 'midi/evt'), T['midi/evt']
+
+for type in *{num, str, (Array 3, num)}
+ describe "#{type}", ->
+ describe ':mk_sig', ->
+ it 'can create value-less Streams', ->
+ stream = type\mk_sig!
+ assert.is.equal 'SigStream', stream.__class.__name
+ assert.is.equal type, stream.type
+ assert.is.nil stream!
+
+ it 'can take an initial value', ->
+ stream = type\mk_sig 4
+ assert.is.equal 'SigStream', stream.__class.__name
+ assert.is.equal type, stream.type
+ assert.is.equal 4, stream!
+
+ describe ':mk_const', ->
+ it 'takes a value', ->
+ stream = type\mk_const 4
+ assert.is.equal 'Constant', stream.__class.__name
+ assert.is.equal type, stream.type
+ assert.is.equal 4, stream!
+
+ describe ':mk_evt', ->
+ stream = type\mk_evt!
+ assert.is.equal 'EvtStream', stream.__class.__name
+ assert.is.equal type, stream.type