aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authors-ol <s-ol@users.noreply.github.com>2020-02-29 13:35:42 +0000
committers-ol <s-ol@users.noreply.github.com>2020-02-29 16:27:34 +0000
commit79aa35d237395cc98c39986b211c57c77b7750e2 (patch)
treec930670dd82bea41a39717a2da906b7f38e326bb
parentdoc and tag spec (diff)
downloadalive-79aa35d237395cc98c39986b211c57c77b7750e2.tar.gz
alive-79aa35d237395cc98c39986b211c57c77b7750e2.zip
add type-pattern matching language
-rw-r--r--core/pattern.moon58
-rw-r--r--core/value.moon2
-rw-r--r--spec/core/pattern_spec.moon189
3 files changed, 249 insertions, 0 deletions
diff --git a/core/pattern.moon b/core/pattern.moon
new file mode 100644
index 0000000..45701e6
--- /dev/null
+++ b/core/pattern.moon
@@ -0,0 +1,58 @@
+unpack or= table.unpack
+
+class Pattern
+ new: (opts) =>
+ if 'string' == type opts
+ splat, const, type, opt = opts\match '^(%*?)(=?)([%w%-%_%/]+)(%??)$'
+ assert type, "couldn't parse type pattern '#{opts}'"
+ opts = {
+ :type
+ splat: splat == '*'
+ const: const == '='
+ opt: opt == '?'
+ }
+
+ @type = opts.type
+ @const = opts.const
+ @opt = opts.opt
+ @splat = opts.splat
+
+ matches: (result) =>
+ return false unless result
+
+ if @const
+ return false unless result\is_const!
+
+ if not result.value
+ return @type == 'nil'
+
+ return true if @type == 'any'
+
+ result.value.type == @type
+
+ match: (results) =>
+ if @splat
+ matched = while @matches results[1]
+ table.remove results, 1
+
+ assert @opt or #matched > 0, "expected at least one argument for spread!"
+ matched
+ else
+ matches = @matches results[1]
+ assert @opt or matches, "couldn't match argument #{results[1]} as type #{@type}!"
+ matches and table.remove results, 1
+
+match = (pattern, results) ->
+ patterns = while pattern
+ pat, rest = pattern\match '^([^ ]+) (.*)$'
+ pat = pattern unless pat
+ pattern = rest
+ Pattern pat
+ values = [p\match results for p in *patterns]
+ assert #results == 0, "#{#results} extra arguments given!"
+ unpack values
+
+{
+ :Pattern
+ :match
+}
diff --git a/core/value.moon b/core/value.moon
index 6fbda65..5379921 100644
--- a/core/value.moon
+++ b/core/value.moon
@@ -37,6 +37,8 @@ class Result
for d in *@op.impulses
@all_impulses[d] = true
+ is_const: => not next @all_impulses
+
-- asserts value-constness and returns the value
const: (msg) =>
assert not (next @all_impulses), msg or "eval-time const expected"
diff --git a/spec/core/pattern_spec.moon b/spec/core/pattern_spec.moon
new file mode 100644
index 0000000..ee56634
--- /dev/null
+++ b/spec/core/pattern_spec.moon
@@ -0,0 +1,189 @@
+import Pattern, match from require 'core.pattern'
+import Result, Value from require 'core.value'
+
+-- wrap in non-const result
+wrap = (value) ->
+ with Result :value
+ .all_impulses = { 'fake' }
+
+-- wrap in const result
+wrap_const = (value) -> Result :value
+
+describe 'Type Pattern', ->
+ num = wrap Value.num 1
+ str = wrap Value.str 'hello'
+ special = wrap Value 'midi/sysex-message'
+ c_num = wrap_const Value.num 1
+ c_str = wrap_const Value.str 'hello'
+
+ describe 'simple types', ->
+ it 'matches self type', ->
+ pat = Pattern 'num'
+ assert.is.true pat\matches num
+ assert.is.true pat\matches c_num
+ assert.is.false pat\matches str
+ assert.is.false pat\matches c_str
+ assert.is.false pat\matches nil
+
+ it 'can contain special symbols', ->
+ pat = Pattern 'midi/sysex-message'
+ assert.is.true pat\matches special
+ assert.is.false pat\matches num
+ assert.is.false pat\matches str
+ assert.is.false pat\matches nil
+
+ it 'can match \'any\' type', ->
+ pat = Pattern 'any'
+ assert.is.true pat\matches num
+ assert.is.true pat\matches str
+ assert.is.true pat\matches special
+ assert.is.true pat\matches c_num
+ assert.is.true pat\matches c_str
+ assert.is.false pat\matches nil
+
+ it 'checks const-ness', ->
+ pat = Pattern '=any'
+ assert.is.true pat.const
+ assert.is.true pat\matches c_num
+ assert.is.true pat\matches c_str
+ assert.is.false pat\matches num
+ assert.is.false pat\matches str
+ assert.is.false pat\matches nil
+
+ pat = Pattern '=num'
+ assert.is.true pat.const
+ assert.is.true pat\matches c_num
+ assert.is.false pat\matches c_str
+ assert.is.false pat\matches num
+ assert.is.false pat\matches str
+ assert.is.false pat\matches nil
+
+ describe 'can capture', ->
+ it 'matches', ->
+ pat = Pattern 'str'
+ stream = {str, num}
+ assert.is.equal str, pat\match stream
+ assert.is.same {num}, stream
+
+ it 'errors if not given', ->
+ pat = Pattern 'str'
+ stream = {}
+ assert.has.error -> pat\match stream
+
+ stream = {num, str}
+ assert.has.error -> pat\match stream
+ assert.is.same {num, str}, stream
+
+ describe 'optional types', ->
+ pat = Pattern 'num?'
+
+ it 'at the end', ->
+ stream = {}
+ assert.is.false, pat\match stream
+
+ it 'in the middle', ->
+ stream = {str}
+ assert.is.false, pat\match stream
+
+ describe 'splats', ->
+ pat = Pattern '*num'
+ it 'parses', ->
+ assert.is.true pat.splat
+ assert.is.equal 'num', pat.type
+
+ it 'at the end', ->
+ stream = {}
+ assert.has.error -> pat\match stream
+ assert.is.same {}, stream
+
+ stream = {num}
+ assert.is.same {num}, pat\match stream
+ assert.is.same {}, stream
+
+ stream = {num, num}
+ assert.is.same {num, num}, pat\match stream
+ assert.is.same {}, stream
+
+ it 'in the middle', ->
+ stream = {str}
+ assert.has.error -> pat\match stream
+ assert.is.same {str}, stream
+
+ stream = {num, str}
+ assert.is.same {num}, pat\match stream
+ assert.is.same {str}, stream
+
+ stream = {num, num, str}
+ assert.is.same {num, num}, pat\match stream
+ assert.is.same {str}, stream
+
+ describe 'optional splats', ->
+ pat = Pattern '*num?'
+ it 'parses', ->
+ assert.is.true pat.splat
+ assert.is.true pat.opt
+ assert.is.equal 'num', pat.type
+
+ it 'at the end', ->
+ stream = {}
+ assert.is.same {}, pat\match stream
+ assert.is.same {}, stream
+
+ stream = {num}
+ assert.is.same {num}, pat\match stream
+ assert.is.same {}, stream
+
+ stream = {num, num}
+ assert.is.same {num, num}, pat\match stream
+ assert.is.same {}, stream
+
+ it 'in the middle', ->
+ stream = {str}
+ assert.is.same {}, pat\match stream
+ assert.is.same {str}, stream
+
+ stream = {num, str}
+ assert.is.same {num}, pat\match stream
+ assert.is.same {str}, stream
+
+ stream = {num, num, str}
+ assert.is.same {num, num}, pat\match stream
+ assert.is.same {str}, stream
+
+describe 'match', ->
+ num = wrap Value.num 1
+ str = wrap Value.str 'hello'
+ c_num = wrap_const Value.num 1
+ c_str = wrap_const Value.str 'hello'
+
+ it 'matches lists', ->
+ assert.is.same {num, num, str}, {match 'num num str', {num, num, str}}
+ assert.is.same {num, str}, {match 'num str', {num, str}}
+ assert.is.same {c_num, str}, {match '=num str', {c_num, str}}
+
+ it 'throws type errors', ->
+ assert.has.error -> match 'str num str', {num, num, str}
+ assert.has.error -> match 'num str num', {num, str}
+ assert.has.error -> match '=num str', {num, str}
+
+ it 'throws extra arg errors', ->
+ assert.has.error -> match 'num str', {num, str, str}
+ assert.has.error -> match {}, {num}
+ assert.has.error -> match '*num', {num, num, str}
+
+ it 'matches optional arguments', ->
+ assert.is.same {str, num, str}, {match 'str num? str', {str, num, str}}
+ assert.is.same {str, false, str}, {match 'str num? str', {str, str}}
+ assert.is.same {str, false}, {match 'str num?', {str}}
+
+ it 'matches splats', ->
+ assert.is.same {{c_str, str, str}, num}, {match '*str num', {c_str, str, str, num}}
+ assert.is.same {c_str, {str, str}}, {match 'any? *str', {c_str, str, str}}
+ assert.has.error -> match '*str num', {num}
+ assert.has.error -> match 'any? *str', {str}
+
+ it 'matches optional splats', ->
+ assert.is.same {{c_str, str, str}, num}, {match '*str? num', {c_str, str, str, num}}
+ assert.is.same {c_str, {str, str}}, {match 'any? *str?', {c_str, str, str}}
+ assert.is.same {{}, num}, {match '*str? num', {num}}
+ assert.is.same {str, {}}, {match 'any? *str?', {str}}