diff options
| author | s-ol <s-ol@users.noreply.github.com> | 2020-02-29 13:35:42 +0000 |
|---|---|---|
| committer | s-ol <s-ol@users.noreply.github.com> | 2020-02-29 16:27:34 +0000 |
| commit | 79aa35d237395cc98c39986b211c57c77b7750e2 (patch) | |
| tree | c930670dd82bea41a39717a2da906b7f38e326bb | |
| parent | doc and tag spec (diff) | |
| download | alive-79aa35d237395cc98c39986b211c57c77b7750e2.tar.gz alive-79aa35d237395cc98c39986b211c57c77b7750e2.zip | |
add type-pattern matching language
| -rw-r--r-- | core/pattern.moon | 58 | ||||
| -rw-r--r-- | core/value.moon | 2 | ||||
| -rw-r--r-- | spec/core/pattern_spec.moon | 189 |
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}} |
