From f023ca56dbf9372464afe0060270fcef85271db0 Mon Sep 17 00:00:00 2001 From: Matthew Hall Date: Thu, 16 Mar 2023 23:11:23 +1300 Subject: I'm too tired for this --- README.md | 2 +- shard.yml | 2 +- spec/__OLD_parcom_spec.cr | 1530 ++++++++++++++++++++++----------------------- spec/parcom_spec.cr | 170 ++++- src/parcom.cr | 56 +- src/parcom/basic.cr | 59 +- 6 files changed, 1046 insertions(+), 773 deletions(-) diff --git a/README.md b/README.md index e75c8d1..520fed5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ # parcom -A simple parser combinator library for Crystal. +A simple parser combinator library for with a dumb name. diff --git a/shard.yml b/shard.yml index e6fd943..6524108 100644 --- a/shard.yml +++ b/shard.yml @@ -1,7 +1,7 @@ name: parcom version: 0.1.0 -description: Simple parser combinator library +description: Simple parser combinator library with a dumb name authors: - Matthew Hall diff --git a/spec/__OLD_parcom_spec.cr b/spec/__OLD_parcom_spec.cr index 25ae2e9..bb9e374 100644 --- a/spec/__OLD_parcom_spec.cr +++ b/spec/__OLD_parcom_spec.cr @@ -1,765 +1,765 @@ -require "./spec_helper" - -require "../src/parcom.cr" - -include Parcom - -describe Tokens do - describe ".from_string" do - it "constructs a Tokens(Char) from a String" do - tokens = Tokens.from_string("abcd") - tokens.tokens.should eq("abcd".chars) - end - end - - describe "#initialize" do - it "wraps an array with the contents of the given iterable" do - set = Set{'a', 'b', 'c', 'd'} - tokens = Tokens.new(set) - tokens.tokens.should eq(set.to_a) - - arr = "abcd".chars - tokens = Tokens.new(arr) - tokens.tokens.should eq(arr) - end - end - - context do - tokens_empty = Tokens.new([] of Char) - tokens = Tokens.from_string("abcd") - - describe "#[]" do - it "returns the token at the given index" do - tokens[2].should eq('c') - expect_raises(IndexError) { tokens_empty[2] } - end - - it "returns a new Tokens similar to Array#[](Int, Int)" do - tokens[1, 5].should eq(Tokens.new(['b', 'c', 'd'])) - expect_raises(IndexError) { tokens_empty[1, 5] } - end - - it "returns a new Tokens similar to Array#[](Range)" do - tokens[1..3].should eq(Tokens.new(['b', 'c', 'd'])) - expect_raises(IndexError) { tokens_empty[1..3] } - end - end - - describe "#[]?" do - it "analogous to `Array#[]?`" do - # we should only need to check the nil-returning cases - tokens_empty[2]?.should be_nil - tokens_empty[1, 5]?.should be_nil - tokens_empty[1..3]?.should be_nil - end - end - - describe "#empty?" do - it "exposes the `#empty?` method of the wrapped array" do - tokens.empty?.should be_false - tokens_empty.empty?.should be_true - end - end - end -end - -describe Result do - describe "#initialize" do - it "sets values for #tokens and #value" do - tokens = Tokens.from_string("esting") - value = 't' - result = Result(Char, Char).new(tokens, value) - - result.tokens.should eq(tokens) - result.value.should eq(value) - end - end -end - -describe Parser do - p = AnyToken(Char).new - - describe "#parse?" do - it "returns `nil` if the parser fails" do - result = p.parse?(Tokens.new([] of Char)) - - result.should be_nil - end - - it "returns a `Result(T, V)` if the parser succeeds" do - tokens = Tokens.from_string("testing") - result = p.parse(tokens) - - result.should be_a(Result(Char, Char)) - end - end -end - -describe Flunk do - describe "#parse" do - it "always fails" do - tokens = Tokens.from_string("testing") - - expect_raises(ParserFail) { Flunk(Char, Char).new.parse(tokens) } - end - end -end - -describe AnyToken do - context do - p = AnyToken(Char).new - - describe "#parse" do - it "succeeds when input is non-empty" do - tokens = Tokens.from_string("testing") - result = p.parse(tokens) - - result.tokens.should eq(tokens[1..]) - result.value.should eq('t') - end - - it "fails when input is empty" do - expect_raises(ParserFail) { p.parse(Tokens.new([] of Char)) } - end - end - end -end - -describe EOF do - p = EOF(Char).new - - describe "#parse" do - it "succeeds when input is empty" do - result = p.parse(Tokens.new([] of Char)) - - result.tokens.empty?.should be_true - result.value.should be_nil - end - - it "fails when input is non-empty" do - tokens = Tokens.from_string("testing") - - expect_raises(ParserFail) { p.parse(tokens) } - end - end -end - -describe Peek do - tokens = Tokens.from_string("testing") - p = AnyToken(Char).new - result_normal = p.parse(tokens) - result_peek = Peek.new(p).parse(tokens) - - describe "#parse" do - it "does not modify the result of the wrapped parser" do - result_peek.value.should eq(result_normal.value) - end - - it "does not consume any input" do - result_peek.tokens.should eq(tokens) - end - end -end - -describe Assert do - test_f = ->(x : Char) { x == 't' } - p = AnyToken(Char).new.assert { |x| x == 't' } - - describe "#parse" do - it "fails if the wrapped parser fails" do - expect_raises(ParserFail) do - p.parse(Tokens.new([] of Char)) - end - end - - it "fails if the result value fails the test" do - tokens = Tokens.from_string("_testing") - - expect_raises(ParserFail) { p.parse(tokens) } - end - - it "succeeds if the wrapped parser succeeds and the test passes" do - tokens = Tokens.from_string("testing") - expected_char = tokens[0] - result = p.parse(tokens) - - result.value.should eq(expected_char) - test_f.call(expected_char).should be_true - end - end -end - -describe Satisfy do - p = Satisfy(Char).new { |x| x == 't' } - - describe "#parse" do - it "fails if the input is empty" do - expect_raises(ParserFail) { p.parse(Tokens.new([] of Char)) } - end - - it "fails if the token fails the test" do - tokens = Tokens.from_string("_testing") - - expect_raises(ParserFail) { p.parse(tokens) } - end - - it "succeeds if the token passes the test" do - tokens = Tokens.from_string("testing") - expected_char = tokens[0] - result = p.parse(tokens) - - result.value.should eq(expected_char) - end - end -end - -describe Token do - tokens = Tokens.from_string("testing") - - describe "#parse" do - it "fails if the input is empty" do - p = Token(Char).new('t') - - expect_raises(ParserFail) { p.parse(Tokens.new([] of Char)) } - end - - it "fails if the token is not the expected token" do - p = Token(Char).new('#') - - expect_raises(ParserFail) { p.parse(tokens) } - end - - it "succeeds if the token is the expected token" do - expected_char = tokens[0] - p = Token(Char).new(expected_char) - result = p.parse(tokens) - - result.value.should eq(expected_char) - end - end -end - -describe Map do - describe "#parse" do - it "fails if the wrapped parser fails" do - p = AnyToken(Char).new.map { |x| x } - - expect_raises(ParserFail) { p.parse(Tokens.new([] of Char)) } - end - - it "changes the result value via the provided proc" do - p = AnyToken(Char).new.map { |x| x.letter? } - - result = p.parse(Tokens.from_string("testing")) - result.value.should be_true - - result = p.parse(Tokens.from_string("_testing")) - result.value.should be_false - end - end -end - -describe Plus do - describe "#parse" do - tokens = Tokens.from_string("testing") - p_t = Token(Char).new('t') - p_e = Token(Char).new('e') - p_at = Token(Char).new('@') - - it "fails if the first parser fails" do - p = p_at + p_e - expect_raises(ParserFail) { p.parse(tokens) } - end - - it "fails if the second parser fails" do - p = p_t + p_at - expect_raises(ParserFail) { p.parse(tokens) } - end - - it "fails if both parsers fail" do - p = p_at + p_at - expect_raises(ParserFail) { p.parse(tokens) } - end - - it "succeeds if both parsers succeed" do - p = p_t + p_e - result = p.parse(tokens) - - result.tokens.should eq(tokens[2..]) - result.value[0].should eq('t') - result.value[1].should eq('e') - end - - it "evaluates parsers from left to right (left associative)" do - p_succeeds = p_t + p_e - p_fails = p_e + p_t - - p_succeeds.parse(tokens) # should not raise an exception - expect_raises(ParserFail) { p_fails.parse(tokens) } - - p_s = Token(Char).new('s') - - r = (p_t + p_e + p_s).parse(tokens) # should not raise an exception - r.value.should be_a({ {Char, Char}, Char}) - - r = (p_t + (p_e + p_s)).parse(tokens) # should not raise an exception - r.value.should be_a({Char, {Char, Char} }) - end - end -end - -# most behavior shouldn't need to be tested -# since it is based on tested bbehavior from -# Plus and Map -describe Left do - describe "#parse" do - it "returns the value of the first parser if both succeed" do - tokens = Tokens.from_string("testing") - letter_t = Token.new('t') - letter_e = Token.new('e') - result = (letter_t << letter_e).parse(tokens) - - result.value.should eq('t') - result.tokens.should eq(tokens[2..]) - end - end -end - -# same deal as Left -describe Right do - describe "#parse" do - it "returns the value of the second parser if both succeed" do - tokens = Tokens.from_string("testing") - letter_t = Token.new('t') - letter_e = Token.new('e') - result = (letter_t >> letter_e).parse(tokens) - - result.value.should eq('e') - result.tokens.should eq(tokens[2..]) - end - end -end - -describe Phrase do - p = Phrase.new(Token.new('t')) - - describe "#parse" do - it "fails if the wrapped parser fails" do - tokens = Tokens.from_string("_") - - expect_raises(ParserFail) { p.parse(tokens) } - end - - it "fails if not all of the input tokens are parsed" do - tokens = Tokens.from_string("tt") - - expect_raises(ParserFail) { p.parse(tokens) } - end - - it "succeeds if the wrapped parser successfully parses all of the input" do - tokens = Tokens.from_string("t") - result = p.parse(tokens) - - result.tokens.empty?.should be_true - result.value.should eq('t') - end - end -end - -describe Recover do - p = Token.new('t').recover('@') - - describe "#parse" do - it "succeeds and returns the wrapped parser's value if it succeeds" do - tokens = Tokens.from_string("testing") - result = p.parse(tokens) - - result.tokens.should eq(tokens[1..]) - result.value.should eq('t') - end - - it "succeeds and returns the default value without modifying the input if the wrapped parser fails" do - tokens = Tokens.from_string("_____") - result = p.parse(tokens) - - result.tokens.should eq(tokens) - result.value.should eq('@') - end - end -end - -describe Optional do - p = Optional.new(Token.new('t')) - - describe "#parse" do - it "succeeds and returns the wrapped parser's value if it succeeds" do - tokens = Tokens.from_string("testing") - result = p.parse(tokens) - - result.tokens.should eq(tokens[1..]) - result.value.should eq('t') - end - - it "succeeds and returns a value of `nil` without modifying the input if the wrapped parser fails" do - tokens = Tokens.from_string("_____") - result = p.parse(tokens) - - result.tokens.should eq(tokens) - result.value.should be_nil - end - end -end - -describe Sequence do - # HACK: ps has to be declared this way due to contravariance - # https://crystal-lang.org/reference/1.7/syntax_and_semantics/inheritance.html#covariance-and-contravariance - ps = [] of Parser(Char, Char) - ps = ps + "abcd".chars.map { |c| Token.new(c) } - p = Sequence.new(ps) - - describe "#parse" do - it "runs each wrapped parser in order, returns each result" do - tokens = Tokens.from_string("abcd") - result = p.parse(tokens) - - result.value.should eq("abcd".chars) - result.tokens.empty?.should be_true - end - - it "fails if any of the wrapped parsers fail" do - fail_strings = ["", "abed", "bbcd", "abce"] - fail_strings.each do |s| - tokens = Tokens.from_string(s) - expect_raises(ParserFail) { p.parse(tokens) } - end - end - - it "succeeds and returns empty array if parser iterable is empty" do - tokens = Tokens.from_string("abcd") - empty_p = Sequence.new([] of Parser(Char, Char)) - result = empty_p.parse(tokens) - - result.value.empty?.should be_true - result.tokens.should eq(tokens) - end - end -end - -describe TokenSeq do - p = TokenSeq.new("test".chars) - - describe "#parse" do - it "fails if the input stream is too short" do - input = Tokens.from_string("") - expect_raises(ParserFail) { p.parse(input) } - end - - it "fails if it encounters an unexpected token" do - input = Tokens.from_string("text") - expect_raises(ParserFail) { p.parse(input) } - end - - it "succeeds if the input starts with the expected tokens" do - input = Tokens.from_string("testing") - result = p.parse(input) - - result.tokens.should eq(input[4..]) - result.value.should eq("test".chars) - end - end -end - -describe Many do - p = Many.new(Token.new('a')) - - describe "#parse" do - it "returns an empty array if the wrapped parser never succeeds" do - tokens = Tokens.from_string("bb") - result = p.parse(tokens) - - result.value.empty?.should be_true - result.tokens.should eq(tokens) - end - - it "stops parsing when the wrapped parser fails, returns all successes" do - tokens = Tokens.from_string("aaabcd") - result = p.parse(tokens) - - result.value.should eq("aaa".chars) - result.tokens.should eq(tokens[3..]) - - tokens = Tokens.from_string("aaa") - result = p.parse(tokens) - - result.value.should eq("aaa".chars) - result.tokens.should eq(tokens[3..]) - end - - it "stops parsing when the wapped parser succeeds without consuming any input" do - a_optional : Parser(Char, Char?) - a_optional = Optional.new(Token.new('a')) - tokens = Tokens.from_string("aaa") - result = Many(Char, Char?).new(a_optional).parse(tokens) - - result.value.should eq("aaa".chars) - result.tokens.should eq(tokens[3..]) - end - end -end - -describe Some do - p = Some.new(Token.new('a')) - describe "#parse" do - it "fails if the wrapped parser never succeeds" do - tokens = Tokens.from_string("") - expect_raises(ParserFail) { p.parse(tokens) } - end - - it "stops parsing when the wrapped parser fails, returns all successes" do - tokens = Tokens.from_string("aaabcd") - result = p.parse(tokens) - - result.value.should eq("aaa".chars) - result.tokens.should eq(tokens[3..]) - - tokens = Tokens.from_string("aaa") - result = p.parse(tokens) - - result.value.should eq("aaa".chars) - result.tokens.should eq(tokens[3..]) - end - end -end - -describe Exactly do - letter_a = Token.new('a') - tokens = Tokens.from_string("aaabcd") - - describe "#parse" do - it "tries to parse exactly n of the wrapper parser" do - p = Exactly.new(3, letter_a) - result = p.parse(tokens) - - result.value.should eq("aaa".chars) - result.tokens.should eq(tokens[3..]) - end - - it "always succeeds with an empty array if n < 1" do - p = Exactly.new(0, letter_a) - result = p.parse(tokens) - - result.value.empty?.should be_true - result.tokens.should eq(tokens) - - p = Exactly.new(-42, letter_a) - result = p.parse(tokens) - - result.value.empty?.should be_true - result.tokens.should eq(tokens) - end - - it "does not take extra matching tokens" do - p = Exactly.new(2, letter_a) - result = p.parse(tokens) - - result.value.should eq("aa".chars) - result.tokens.should eq(tokens[2..]) - end - - it "fails if there are not enough matching tokens" do - p = Exactly.new(60, letter_a) - expect_raises(ParserFail) { p.parse(tokens) } - end - end -end - -describe AtLeast do - letter_a = Token.new('a') - tokens = Tokens.from_string("aaaabcd") - - describe "#parse" do - it "fails if there are not enough matching tokens to parse" do - p = AtLeast.new(5, letter_a) - expect_raises(ParserFail) { p.parse(tokens) } - #expect_raises(ParserFail) { raise ParserFail.new("sdgseg") } - end - - it "parses n or more times with the given parser" do - p0 = AtLeast.new(0, letter_a) - p2 = AtLeast.new(2, letter_a) - p4 = AtLeast.new(4, letter_a) - - result0 = p0.parse(tokens) - result2 = p2.parse(tokens) - result4 = p4.parse(tokens) - - result0.value.should eq("aaaa".chars) - result0.tokens.should eq(tokens[4..]) - - result2.should eq(result0) - result4.should eq(result0) - end - end -end - -describe AtMost do - letter_a = Token.new('a') - tokens = Tokens.from_string("aaaabcd") - - describe "#parse" do - it "does not parse more than n times" do - p0 = AtMost.new(0, letter_a) - p2 = AtMost.new(2, letter_a) - p6 = AtMost.new(6, letter_a) - - r0 = p0.parse(tokens) - r0.value.empty?.should be_true - r0.tokens.should eq(tokens) - - r2 = p2.parse(tokens) - r2.value.should eq("aa".chars) - r2.tokens.should eq(tokens[2..]) - - r6 = p6.parse(tokens) - r6.value.should eq("aaaa".chars) - r6.tokens.should eq(tokens[4..]) - end - end -end - -describe Between do - letter_a = Token.new('a') - tokens = Tokens.from_string("aaaabcd") - - describe "#parse" do - it "parses at least i times, up to a limit of j times" do - p0_4 = Between.new(0, 4, letter_a) - r0_4 = p0_4.parse(tokens) - - r0_4.value.should eq("aaaa".chars) - r0_4.tokens.should eq(tokens[4..]) - end - - it "fails if there are not enough parser successes" do - p = Between.new(5, 6, letter_a) - expect_raises(ParserFail) { p.parse(tokens) } - end - end -end - -describe FirstOf do - tokens = Tokens.from_string("abcd") - letter_a = Token.new('a') - f = Flunk(Char, Char).new - - describe "#parse" do - it "cannot be instantiated with an empty Enumerable" do - expect_raises(ArgumentError) { FirstOf.new([] of Parser(Char, Char)) } - end - - it "uses the result of the first successful parser" do - a1 = [letter_a, f, f, f] of Parser(Char, Char) - a2 = [f, letter_a, f, f] of Parser(Char, Char) - a3 = [f, f, letter_a, f] of Parser(Char, Char) - a4 = [f, f, f, letter_a] of Parser(Char, Char) - - [a1, a2, a3, a4].each do |arr| - p = FirstOf.new(arr) - r = p.parse(tokens) - r.value.should eq('a') - r.tokens.should eq(tokens[1..]) - end - end - - it "only fails if no parsers are successful" do - x = Token.new('x') - y = Token.new('x') - z = Token.new('x') - p = FirstOf.new([x, y, z] of Parser(Char, Char)) - expect_raises(ParserFail) { p.parse(tokens) } - end - end -end - -describe SepBy do - describe "#parse" do - letter_a = Token.new('a') - comma = Token.new(',') - tokens = Tokens.from_string("a,a,a,a") - - it "fails if no elements can be parsed" do - p = SepBy(Char, Char, Char).new(comma, comma) - expect_raises(ParserFail) { p.parse(tokens) } - end - - it "succeeds if only one element can be parsed" do - t1 = Tokens.from_string("a") - t2 = Tokens.from_string("a,") - p = SepBy(Char, Char, Char).new(letter_a, comma) - - result = p.parse(t1) - result.value.should eq(['a']) - result.tokens.should eq(t1[1..]) - - result = p.parse(t2) - result.value.should eq(['a']) - result.tokens.should eq(t2[1..]) - end - - it "parses 1 element, then 0 or more (sep >> element)" do - p = SepBy(Char, Char, Char).new(letter_a, comma) - - result = p.parse(tokens) - result.value.should eq("aaaa".chars) - result.tokens.empty?.should be_true - - # drop last char in tokens, should parse three elements - result = p.parse(tokens[..5]) - result.value.should eq("aaa".chars) - result.tokens.should eq(Tokens.from_string(",")) - end - end -end - -describe "Practical use" do - describe "Use case: text surrounded by whitespace" do - space = Satisfy(Char).new { |c| c.whitespace? } - non_space = Satisfy(Char).new { |c| !c.whitespace? } - - # TODO: Figure out why mapping on this parser breaks - # initialization of `body`. - word_chars = Some.new(non_space) - ws = Some.new(space) - - bookend = Optional.new(ws) - body = SepBy.new(word_chars, ws) - tokenizer = (bookend >> body << bookend).map do |arrs| - arrs.map { |chars| chars.join } - end - - good_strings = [ - "test with no trailing whitespace", - " test with whitespace in the front", - "test with whitespace in the back", - " test surrounded by whitespace ", - ] - - good_strings.each do |s| - tokens = Tokens.from_string(s) - result = tokenizer.parse(tokens) - result.value.should eq(s.strip.split(/\s+/)) - result.tokens.empty?.should be_true - end - - bad_strings = [ - "", - " ", - ] - - bad_strings.each do |s| - tokens = Tokens.from_string(s) - expect_raises(ParserFail) { tokenizer.parse(tokens) } - end - end -end - +#require "./spec_helper" +# +#require "../src/parcom.cr" +# +#include Parcom +# +#pending Tokens do +# pending ".from_string" do +# it "constructs a Tokens(Char) from a String" do +# tokens = Tokens.from_string("abcd") +# tokens.tokens.should eq("abcd".chars) +# end +# end +# +# pending "#initialize" do +# it "wraps an array with the contents of the given iterable" do +# set = Set{'a', 'b', 'c', 'd'} +# tokens = Tokens.new(set) +# tokens.tokens.should eq(set.to_a) +# +# arr = "abcd".chars +# tokens = Tokens.new(arr) +# tokens.tokens.should eq(arr) +# end +# end +# +# context do +# tokens_empty = Tokens.new([] of Char) +# tokens = Tokens.from_string("abcd") +# +# pending "#[]" do +# it "returns the token at the given index" do +# tokens[2].should eq('c') +# expect_raises(IndexError) { tokens_empty[2] } +# end +# +# it "returns a new Tokens similar to Array#[](Int, Int)" do +# tokens[1, 5].should eq(Tokens.new(['b', 'c', 'd'])) +# expect_raises(IndexError) { tokens_empty[1, 5] } +# end +# +# it "returns a new Tokens similar to Array#[](Range)" do +# tokens[1..3].should eq(Tokens.new(['b', 'c', 'd'])) +# expect_raises(IndexError) { tokens_empty[1..3] } +# end +# end +# +# pending "#[]?" do +# it "analogous to `Array#[]?`" do +# # we should only need to check the nil-returning cases +# tokens_empty[2]?.should be_nil +# tokens_empty[1, 5]?.should be_nil +# tokens_empty[1..3]?.should be_nil +# end +# end +# +# pending "#empty?" do +# it "exposes the `#empty?` method of the wrapped array" do +# tokens.empty?.should be_false +# tokens_empty.empty?.should be_true +# end +# end +# end +#end +# +#pending Result do +# pending "#initialize" do +# it "sets values for #tokens and #value" do +# tokens = Tokens.from_string("esting") +# value = 't' +# result = Result(Char, Char).new(tokens, value) +# +# result.tokens.should eq(tokens) +# result.value.should eq(value) +# end +# end +#end +# +#pending Parser do +# p = AnyToken(Char).new +# +# pending "#parse?" do +# it "returns `nil` if the parser fails" do +# result = p.parse?(Tokens.new([] of Char)) +# +# result.should be_nil +# end +# +# it "returns a `Result(T, V)` if the parser succeeds" do +# tokens = Tokens.from_string("testing") +# result = p.parse(tokens) +# +# result.should be_a(Result(Char, Char)) +# end +# end +#end +# +#pending Flunk do +# pending "#parse" do +# it "always fails" do +# tokens = Tokens.from_string("testing") +# +# expect_raises(ParserFail) { Flunk(Char, Char).new.parse(tokens) } +# end +# end +#end +# +#pending AnyToken do +# context do +# p = AnyToken(Char).new +# +# pending "#parse" do +# it "succeeds when input is non-empty" do +# tokens = Tokens.from_string("testing") +# result = p.parse(tokens) +# +# result.tokens.should eq(tokens[1..]) +# result.value.should eq('t') +# end +# +# it "fails when input is empty" do +# expect_raises(ParserFail) { p.parse(Tokens.new([] of Char)) } +# end +# end +# end +#end +# +#pending EOF do +# p = EOF(Char).new +# +# pending "#parse" do +# it "succeeds when input is empty" do +# result = p.parse(Tokens.new([] of Char)) +# +# result.tokens.empty?.should be_true +# result.value.should be_nil +# end +# +# it "fails when input is non-empty" do +# tokens = Tokens.from_string("testing") +# +# expect_raises(ParserFail) { p.parse(tokens) } +# end +# end +#end +# +#pending Peek do +# tokens = Tokens.from_string("testing") +# p = AnyToken(Char).new +# result_normal = p.parse(tokens) +# result_peek = Peek.new(p).parse(tokens) +# +# pending "#parse" do +# it "does not modify the result of the wrapped parser" do +# result_peek.value.should eq(result_normal.value) +# end +# +# it "does not consume any input" do +# result_peek.tokens.should eq(tokens) +# end +# end +#end +# +#pending Assert do +# test_f = ->(x : Char) { x == 't' } +# p = AnyToken(Char).new.assert { |x| x == 't' } +# +# pending "#parse" do +# it "fails if the wrapped parser fails" do +# expect_raises(ParserFail) do +# p.parse(Tokens.new([] of Char)) +# end +# end +# +# it "fails if the result value fails the test" do +# tokens = Tokens.from_string("_testing") +# +# expect_raises(ParserFail) { p.parse(tokens) } +# end +# +# it "succeeds if the wrapped parser succeeds and the test passes" do +# tokens = Tokens.from_string("testing") +# expected_char = tokens[0] +# result = p.parse(tokens) +# +# result.value.should eq(expected_char) +# test_f.call(expected_char).should be_true +# end +# end +#end +# +#pending Satisfy do +# p = Satisfy(Char).new { |x| x == 't' } +# +# pending "#parse" do +# it "fails if the input is empty" do +# expect_raises(ParserFail) { p.parse(Tokens.new([] of Char)) } +# end +# +# it "fails if the token fails the test" do +# tokens = Tokens.from_string("_testing") +# +# expect_raises(ParserFail) { p.parse(tokens) } +# end +# +# it "succeeds if the token passes the test" do +# tokens = Tokens.from_string("testing") +# expected_char = tokens[0] +# result = p.parse(tokens) +# +# result.value.should eq(expected_char) +# end +# end +#end +# +#pending Token do +# tokens = Tokens.from_string("testing") +# +# pending "#parse" do +# it "fails if the input is empty" do +# p = Token(Char).new('t') +# +# expect_raises(ParserFail) { p.parse(Tokens.new([] of Char)) } +# end +# +# it "fails if the token is not the expected token" do +# p = Token(Char).new('#') +# +# expect_raises(ParserFail) { p.parse(tokens) } +# end +# +# it "succeeds if the token is the expected token" do +# expected_char = tokens[0] +# p = Token(Char).new(expected_char) +# result = p.parse(tokens) +# +# result.value.should eq(expected_char) +# end +# end +#end +# +#pending Map do +# pending "#parse" do +# it "fails if the wrapped parser fails" do +# p = AnyToken(Char).new.map { |x| x } +# +# expect_raises(ParserFail) { p.parse(Tokens.new([] of Char)) } +# end +# +# it "changes the result value via the provided proc" do +# p = AnyToken(Char).new.map { |x| x.letter? } +# +# result = p.parse(Tokens.from_string("testing")) +# result.value.should be_true +# +# result = p.parse(Tokens.from_string("_testing")) +# result.value.should be_false +# end +# end +#end +# +#pending Plus do +# pending "#parse" do +# tokens = Tokens.from_string("testing") +# p_t = Token(Char).new('t') +# p_e = Token(Char).new('e') +# p_at = Token(Char).new('@') +# +# it "fails if the first parser fails" do +# p = p_at + p_e +# expect_raises(ParserFail) { p.parse(tokens) } +# end +# +# it "fails if the second parser fails" do +# p = p_t + p_at +# expect_raises(ParserFail) { p.parse(tokens) } +# end +# +# it "fails if both parsers fail" do +# p = p_at + p_at +# expect_raises(ParserFail) { p.parse(tokens) } +# end +# +# it "succeeds if both parsers succeed" do +# p = p_t + p_e +# result = p.parse(tokens) +# +# result.tokens.should eq(tokens[2..]) +# result.value[0].should eq('t') +# result.value[1].should eq('e') +# end +# +# it "evaluates parsers from left to right (left associative)" do +# p_succeeds = p_t + p_e +# p_fails = p_e + p_t +# +# p_succeeds.parse(tokens) # should not raise an exception +# expect_raises(ParserFail) { p_fails.parse(tokens) } +# +# p_s = Token(Char).new('s') +# +# r = (p_t + p_e + p_s).parse(tokens) # should not raise an exception +# r.value.should be_a({ {Char, Char}, Char}) +# +# r = (p_t + (p_e + p_s)).parse(tokens) # should not raise an exception +# r.value.should be_a({Char, {Char, Char} }) +# end +# end +#end +# +## most behavior shouldn't need to be tested +## since it is based on tested bbehavior from +## Plus and Map +#pending Left do +# pending "#parse" do +# it "returns the value of the first parser if both succeed" do +# tokens = Tokens.from_string("testing") +# letter_t = Token.new('t') +# letter_e = Token.new('e') +# result = (letter_t << letter_e).parse(tokens) +# +# result.value.should eq('t') +# result.tokens.should eq(tokens[2..]) +# end +# end +#end +# +## same deal as Left +#pending Right do +# pending "#parse" do +# it "returns the value of the second parser if both succeed" do +# tokens = Tokens.from_string("testing") +# letter_t = Token.new('t') +# letter_e = Token.new('e') +# result = (letter_t >> letter_e).parse(tokens) +# +# result.value.should eq('e') +# result.tokens.should eq(tokens[2..]) +# end +# end +#end +# +#pending Phrase do +# p = Phrase.new(Token.new('t')) +# +# pending "#parse" do +# it "fails if the wrapped parser fails" do +# tokens = Tokens.from_string("_") +# +# expect_raises(ParserFail) { p.parse(tokens) } +# end +# +# it "fails if not all of the input tokens are parsed" do +# tokens = Tokens.from_string("tt") +# +# expect_raises(ParserFail) { p.parse(tokens) } +# end +# +# it "succeeds if the wrapped parser successfully parses all of the input" do +# tokens = Tokens.from_string("t") +# result = p.parse(tokens) +# +# result.tokens.empty?.should be_true +# result.value.should eq('t') +# end +# end +#end +# +#pending Recover do +# p = Token.new('t').recover('@') +# +# pending "#parse" do +# it "succeeds and returns the wrapped parser's value if it succeeds" do +# tokens = Tokens.from_string("testing") +# result = p.parse(tokens) +# +# result.tokens.should eq(tokens[1..]) +# result.value.should eq('t') +# end +# +# it "succeeds and returns the default value without modifying the input if the wrapped parser fails" do +# tokens = Tokens.from_string("_____") +# result = p.parse(tokens) +# +# result.tokens.should eq(tokens) +# result.value.should eq('@') +# end +# end +#end +# +#pending Optional do +# p = Optional.new(Token.new('t')) +# +# pending "#parse" do +# it "succeeds and returns the wrapped parser's value if it succeeds" do +# tokens = Tokens.from_string("testing") +# result = p.parse(tokens) +# +# result.tokens.should eq(tokens[1..]) +# result.value.should eq('t') +# end +# +# it "succeeds and returns a value of `nil` without modifying the input if the wrapped parser fails" do +# tokens = Tokens.from_string("_____") +# result = p.parse(tokens) +# +# result.tokens.should eq(tokens) +# result.value.should be_nil +# end +# end +#end +# +#pending Sequence do +# # HACK: ps has to be declared this way due to contravariance +# # https://crystal-lang.org/reference/1.7/syntax_and_semantics/inheritance.html#covariance-and-contravariance +# ps = [] of Parser(Char, Char) +# ps = ps + "abcd".chars.map { |c| Token.new(c) } +# p = Sequence.new(ps) +# +# pending "#parse" do +# it "runs each wrapped parser in order, returns each result" do +# tokens = Tokens.from_string("abcd") +# result = p.parse(tokens) +# +# result.value.should eq("abcd".chars) +# result.tokens.empty?.should be_true +# end +# +# it "fails if any of the wrapped parsers fail" do +# fail_strings = ["", "abed", "bbcd", "abce"] +# fail_strings.each do |s| +# tokens = Tokens.from_string(s) +# expect_raises(ParserFail) { p.parse(tokens) } +# end +# end +# +# it "succeeds and returns empty array if parser iterable is empty" do +# tokens = Tokens.from_string("abcd") +# empty_p = Sequence.new([] of Parser(Char, Char)) +# result = empty_p.parse(tokens) +# +# result.value.empty?.should be_true +# result.tokens.should eq(tokens) +# end +# end +#end +# +#pending TokenSeq do +# p = TokenSeq.new("test".chars) +# +# pending "#parse" do +# it "fails if the input stream is too short" do +# input = Tokens.from_string("") +# expect_raises(ParserFail) { p.parse(input) } +# end +# +# it "fails if it encounters an unexpected token" do +# input = Tokens.from_string("text") +# expect_raises(ParserFail) { p.parse(input) } +# end +# +# it "succeeds if the input starts with the expected tokens" do +# input = Tokens.from_string("testing") +# result = p.parse(input) +# +# result.tokens.should eq(input[4..]) +# result.value.should eq("test".chars) +# end +# end +#end +# +#pending Many do +# p = Many.new(Token.new('a')) +# +# pending "#parse" do +# it "returns an empty array if the wrapped parser never succeeds" do +# tokens = Tokens.from_string("bb") +# result = p.parse(tokens) +# +# result.value.empty?.should be_true +# result.tokens.should eq(tokens) +# end +# +# it "stops parsing when the wrapped parser fails, returns all successes" do +# tokens = Tokens.from_string("aaabcd") +# result = p.parse(tokens) +# +# result.value.should eq("aaa".chars) +# result.tokens.should eq(tokens[3..]) +# +# tokens = Tokens.from_string("aaa") +# result = p.parse(tokens) +# +# result.value.should eq("aaa".chars) +# result.tokens.should eq(tokens[3..]) +# end +# +# it "stops parsing when the wapped parser succeeds without consuming any input" do +# a_optional : Parser(Char, Char?) +# a_optional = Optional.new(Token.new('a')) +# tokens = Tokens.from_string("aaa") +# result = Many(Char, Char?).new(a_optional).parse(tokens) +# +# result.value.should eq("aaa".chars) +# result.tokens.should eq(tokens[3..]) +# end +# end +#end +# +#pending Some do +# p = Some.new(Token.new('a')) +# pending "#parse" do +# it "fails if the wrapped parser never succeeds" do +# tokens = Tokens.from_string("") +# expect_raises(ParserFail) { p.parse(tokens) } +# end +# +# it "stops parsing when the wrapped parser fails, returns all successes" do +# tokens = Tokens.from_string("aaabcd") +# result = p.parse(tokens) +# +# result.value.should eq("aaa".chars) +# result.tokens.should eq(tokens[3..]) +# +# tokens = Tokens.from_string("aaa") +# result = p.parse(tokens) +# +# result.value.should eq("aaa".chars) +# result.tokens.should eq(tokens[3..]) +# end +# end +#end +# +#pending Exactly do +# letter_a = Token.new('a') +# tokens = Tokens.from_string("aaabcd") +# +# pending "#parse" do +# it "tries to parse exactly n of the wrapper parser" do +# p = Exactly.new(3, letter_a) +# result = p.parse(tokens) +# +# result.value.should eq("aaa".chars) +# result.tokens.should eq(tokens[3..]) +# end +# +# it "always succeeds with an empty array if n < 1" do +# p = Exactly.new(0, letter_a) +# result = p.parse(tokens) +# +# result.value.empty?.should be_true +# result.tokens.should eq(tokens) +# +# p = Exactly.new(-42, letter_a) +# result = p.parse(tokens) +# +# result.value.empty?.should be_true +# result.tokens.should eq(tokens) +# end +# +# it "does not take extra matching tokens" do +# p = Exactly.new(2, letter_a) +# result = p.parse(tokens) +# +# result.value.should eq("aa".chars) +# result.tokens.should eq(tokens[2..]) +# end +# +# it "fails if there are not enough matching tokens" do +# p = Exactly.new(60, letter_a) +# expect_raises(ParserFail) { p.parse(tokens) } +# end +# end +#end +# +#pending AtLeast do +# letter_a = Token.new('a') +# tokens = Tokens.from_string("aaaabcd") +# +# pending "#parse" do +# it "fails if there are not enough matching tokens to parse" do +# p = AtLeast.new(5, letter_a) +# expect_raises(ParserFail) { p.parse(tokens) } +# #expect_raises(ParserFail) { raise ParserFail.new("sdgseg") } +# end +# +# it "parses n or more times with the given parser" do +# p0 = AtLeast.new(0, letter_a) +# p2 = AtLeast.new(2, letter_a) +# p4 = AtLeast.new(4, letter_a) +# +# result0 = p0.parse(tokens) +# result2 = p2.parse(tokens) +# result4 = p4.parse(tokens) +# +# result0.value.should eq("aaaa".chars) +# result0.tokens.should eq(tokens[4..]) +# +# result2.should eq(result0) +# result4.should eq(result0) +# end +# end +#end +# +#pending AtMost do +# letter_a = Token.new('a') +# tokens = Tokens.from_string("aaaabcd") +# +# pending "#parse" do +# it "does not parse more than n times" do +# p0 = AtMost.new(0, letter_a) +# p2 = AtMost.new(2, letter_a) +# p6 = AtMost.new(6, letter_a) +# +# r0 = p0.parse(tokens) +# r0.value.empty?.should be_true +# r0.tokens.should eq(tokens) +# +# r2 = p2.parse(tokens) +# r2.value.should eq("aa".chars) +# r2.tokens.should eq(tokens[2..]) +# +# r6 = p6.parse(tokens) +# r6.value.should eq("aaaa".chars) +# r6.tokens.should eq(tokens[4..]) +# end +# end +#end +# +#pending Between do +# letter_a = Token.new('a') +# tokens = Tokens.from_string("aaaabcd") +# +# pending "#parse" do +# it "parses at least i times, up to a limit of j times" do +# p0_4 = Between.new(0, 4, letter_a) +# r0_4 = p0_4.parse(tokens) +# +# r0_4.value.should eq("aaaa".chars) +# r0_4.tokens.should eq(tokens[4..]) +# end +# +# it "fails if there are not enough parser successes" do +# p = Between.new(5, 6, letter_a) +# expect_raises(ParserFail) { p.parse(tokens) } +# end +# end +#end +# +#pending FirstOf do +# tokens = Tokens.from_string("abcd") +# letter_a = Token.new('a') +# f = Flunk(Char, Char).new +# +# pending "#parse" do +# it "cannot be instantiated with an empty Enumerable" do +# expect_raises(ArgumentError) { FirstOf.new([] of Parser(Char, Char)) } +# end +# +# it "uses the result of the first successful parser" do +# a1 = [letter_a, f, f, f] of Parser(Char, Char) +# a2 = [f, letter_a, f, f] of Parser(Char, Char) +# a3 = [f, f, letter_a, f] of Parser(Char, Char) +# a4 = [f, f, f, letter_a] of Parser(Char, Char) +# +# [a1, a2, a3, a4].each do |arr| +# p = FirstOf.new(arr) +# r = p.parse(tokens) +# r.value.should eq('a') +# r.tokens.should eq(tokens[1..]) +# end +# end +# +# it "only fails if no parsers are successful" do +# x = Token.new('x') +# y = Token.new('x') +# z = Token.new('x') +# p = FirstOf.new([x, y, z] of Parser(Char, Char)) +# expect_raises(ParserFail) { p.parse(tokens) } +# end +# end +#end +# +#pending SepBy do +# pending "#parse" do +# letter_a = Token.new('a') +# comma = Token.new(',') +# tokens = Tokens.from_string("a,a,a,a") +# +# it "fails if no elements can be parsed" do +# p = SepBy(Char, Char, Char).new(comma, comma) +# expect_raises(ParserFail) { p.parse(tokens) } +# end +# +# it "succeeds if only one element can be parsed" do +# t1 = Tokens.from_string("a") +# t2 = Tokens.from_string("a,") +# p = SepBy(Char, Char, Char).new(letter_a, comma) +# +# result = p.parse(t1) +# result.value.should eq(['a']) +# result.tokens.should eq(t1[1..]) +# +# result = p.parse(t2) +# result.value.should eq(['a']) +# result.tokens.should eq(t2[1..]) +# end +# +# it "parses 1 element, then 0 or more (sep >> element)" do +# p = SepBy(Char, Char, Char).new(letter_a, comma) +# +# result = p.parse(tokens) +# result.value.should eq("aaaa".chars) +# result.tokens.empty?.should be_true +# +# # drop last char in tokens, should parse three elements +# result = p.parse(tokens[..5]) +# result.value.should eq("aaa".chars) +# result.tokens.should eq(Tokens.from_string(",")) +# end +# end +#end +# +#pending "Practical use" do +# pending "Use case: text surrounded by whitespace" do +# space = Satisfy(Char).new { |c| c.whitespace? } +# non_space = Satisfy(Char).new { |c| !c.whitespace? } +# +# # TODO: Figure out why mapping on this parser breaks +# # initialization of `body`. +# word_chars = Some.new(non_space) +# ws = Some.new(space) +# +# bookend = Optional.new(ws) +# body = SepBy.new(word_chars, ws) +# tokenizer = (bookend >> body << bookend).map do |arrs| +# arrs.map { |chars| chars.join } +# end +# +# good_strings = [ +# "test with no trailing whitespace", +# " test with whitespace in the front", +# "test with whitespace in the back", +# " test surrounded by whitespace ", +# ] +# +# good_strings.each do |s| +# tokens = Tokens.from_string(s) +# result = tokenizer.parse(tokens) +# result.value.should eq(s.strip.split(/\s+/)) +# result.tokens.empty?.should be_true +# end +# +# bad_strings = [ +# "", +# " ", +# ] +# +# bad_strings.each do |s| +# tokens = Tokens.from_string(s) +# expect_raises(ParserFail) { tokenizer.parse(tokens) } +# end +# end +#end +# diff --git a/spec/parcom_spec.cr b/spec/parcom_spec.cr index d25c786..ceb7de8 100644 --- a/spec/parcom_spec.cr +++ b/spec/parcom_spec.cr @@ -4,6 +4,174 @@ require "../src/parcom.cr" include Parcom -pending State do +describe Tokens do + describe ".from_string" do + it "constructs a Tokens(Char) from a String" do + tokens = Tokens.from_string("abcd") + tokens.tokens.should eq("abcd".chars) + end + end + + describe "#initialize" do + it "wraps an array with the contents of the given iterable" do + set = Set{'a', 'b', 'c', 'd'} + tokens = Tokens.new(set) + tokens.tokens.should eq(set.to_a) + + arr = "abcd".chars + tokens = Tokens.new(arr) + tokens.tokens.should eq(arr) + end + end + + context do + tokens_empty = Tokens.new([] of Char) + tokens = Tokens.from_string("abcd") + + describe "#[]" do + it "returns the token at the given index" do + tokens[2].should eq('c') + expect_raises(IndexError) { tokens_empty[2] } + end + + it "returns a new Tokens similar to Array#[](Int, Int)" do + tokens[1, 5].should eq(Tokens.new(['b', 'c', 'd'])) + expect_raises(IndexError) { tokens_empty[1, 5] } + end + + it "returns a new Tokens similar to Array#[](Range)" do + tokens[1..3].should eq(Tokens.new(['b', 'c', 'd'])) + expect_raises(IndexError) { tokens_empty[1..3] } + end + end + + describe "#[]?" do + it "analogous to `Array#[]?`" do + # we should only need to check the nil-returning cases + tokens_empty[2]?.should be_nil + tokens_empty[1, 5]?.should be_nil + tokens_empty[1..3]?.should be_nil + end + end + + describe "#empty?" do + it "exposes the `#empty?` method of the wrapped array" do + tokens.empty?.should be_false + tokens_empty.empty?.should be_true + end + end + end +end + +describe Result do + describe "#map" do + r = Result.new(Tokens.from_string("abcd"), 'x') + r_expected = Result.new(Tokens.from_string("abcd"), 'x'.ord) + + it "accepts a proc" do + f = ->(c : Char) { c.ord } + r.map(f).should eq(r_expected) + end + + it "accepts a block" do + r.map { |c| c.ord }.should eq(r_expected) + end + end +end + +pending Parser do + describe "#assert" do + p = Basic(Char, Char).any_token.assert { |c| c == 'a' } + + it "succeeds if the parser succeeds and if the predicate passes" do + tokens = Tokens.from_string("abcd") + result = p.parse(tokens) + + result.value.should eq(tokens[0]) + result.tokens.should eq(tokens[1..]) + end + + it "fails if the predicate fails" do + tokens = Tokens.from_string("bbcd") + expect_raises(ParserFail) { p.parse(tokens) } + end + end +end + +describe Basic do + describe "Basic.pure" do + v = 'a' + p = Basic(Char, Char).pure(v) + tokens = Tokens.from_string("____") + result = p.parse(tokens) + + it "returns a value of whatever it was initialized with" do + result.value.should eq(v) + end + + it "does not modify the input" do + result.tokens.should eq(tokens) + end + end + + describe "Basic.flunk" do + p = Basic(Char, Char).flunk + it "always fails" do + expect_raises(ParserFail) { p.parse(Tokens.from_string("arbitrary")) } + end + end + + describe "Basic.any_token" do + p = Basic(Char, Nil).any_token + + it "parses the first token in the input stream" do + tokens = Tokens.from_string("abcd") + result = p.parse(tokens) + + result.value.should eq(tokens[0]) + result.tokens.should eq(tokens[1..]) + end + + it "fails if the input stream is empty" do + tokens = Tokens.from_string("") + expect_raises(ParserFail) { p.parse(tokens) } + end + end + + describe "Basic.eof" do + p = Basic(Char, Nil).eof + + it "succeeds with nil if the input stream is empty" do + tokens = Tokens.from_string("") + result = p.parse(tokens) + + result.value.should be_nil + result.tokens.empty?.should be_true + end + + it "fails if the input stream is not empty" do + tokens = Tokens.from_string("____") + expect_raises(ParserFail) { p.parse(tokens) } + end + end + + # TODO: the type checker hates me + describe "Basic.satisfy" do + #p = Basic.satisfy(Char) { |c| c == 'a' } + p = Basic(Char, Char).satisfy(->(c : Char) { c == 'a' }) + + it "succeeds if the token passes the predicate" do + tokens = Tokens.from_string("abcd") + result = p.parse(tokens).as_a(Result(Char, Char)) + + result.value.should eq(tokens[0]) + result.tokens.should eq(tokens[1..]) + end + + it "fails if the token fails the predicate" do + tokens = Tokens.from_string("bbcd") + expect_raises(ParserFail) { p.parse(tokens) } + end + end end diff --git a/src/parcom.cr b/src/parcom.cr index d12b011..ffa5654 100644 --- a/src/parcom.cr +++ b/src/parcom.cr @@ -68,9 +68,43 @@ module Parcom def initialize(@tokens : Tokens(T), @value : U) end + + def map(f : U -> V) : Result(T, V) forall V + Result.new(@tokens, f.call(@value)) + end + + def map(&block : U -> V) : Result(T, V) forall V + map(block) + end end - class Parser(T, U) + # A parser defines a process of extracting some sort of data + # from a stream of tokens. + # `Parser` objects wrap a function/block that accepts a `Tokens` object, + # and either returns a `Result`, or raises a `ParserFail`. + # This struct also defines a handful of methods for modifying a parser's + # behavior. + # + # The function that is wrapped by a parser should: + # 1. Have the type `Tokens(T) -> Result(T, U)` + # 1. Indicate failure by raising a `ParserFail` + # 1. Not introduce side-effects + # + # Instead of inheriting from `Parser`, custom parsers should be + # defined using a method that generates the parser directly: + # ``` + # struct Foo(T, U) < Parser(T, Array(U)) + # def parse(tokens) + # # This will cause headaches + # end + # end + # + # # Do this instead: + # def foo(t : T.class, u : U.class, p : Parser(T, U)) : Parser(T, U) forall T, U + # Parser(T, U).new("Foo") { |tokens| "your code here" } + # end + # ``` + struct Parser(T, U) getter name def initialize(@name : String, @f : Tokens(T) -> Result(T, U)) @@ -80,6 +114,11 @@ module Parcom @f = block end + # Changes the `name` property and returns `self`. + # This should be used to specify a custom name: + # ``` + # a = Basic.token(Char, 'a').named("letter a") + # ``` def named(name : String) : self @name = name self @@ -89,7 +128,7 @@ module Parcom @f.call(tokens) end - def parse(tokens : Tokens(T)) : Result(T, U)? + def parse?(tokens : Tokens(T)) : Result(T, U)? parse(tokens) rescue nil @@ -97,7 +136,7 @@ module Parcom def assert(f : U -> Bool) : Parser(T, U) p = self - Parser.new("#{p.name} assertion") do |tokens| + Parser.new("#{p.name} (assertion)") do |tokens| result = p.parse(tokens) unless f.call(r.value) raise ParserFail.new("Assertion failed for value #{r.value}") @@ -109,6 +148,17 @@ module Parcom def assert(&block : U -> Bool) : Parser(T, U) assert(block) end + + def map(f : U -> T) : Parser(T, V) forall V + p = self + Parser(T, V).new("#{p.name} (mapped)") do |tokens| + p.parse(tokens).map(f) + end + end + + def map(&block : U -> T) : Parser(T, V) forall V + map(block) + end end end diff --git a/src/parcom/basic.cr b/src/parcom/basic.cr index 6594555..4f01021 100644 --- a/src/parcom/basic.cr +++ b/src/parcom/basic.cr @@ -1,9 +1,64 @@ require "../parcom.cr" module Parcom - module Basic + module Basic(T, U) + # A collection of building-block parsers that can be combined into more + # powerful parsers. If you can't find a parser here that does what you're + # looking for, it may be defined as a method of the `Parser` struct itself. + extend self + + # Creates a parser that always succeeds with the given value. + # This parser consumes no input. def pure(value : U) : Parser(T, U) forall T, U - Parser.new("Pure #{value}") { |tokens| Result.new(tokens, value) } + Parser(T, U).new("Pure #{value}") { |tokens| Result.new(tokens, value) } + end + + # Creates a parser that always fails. + def flunk : Parser(T, U) forall T, U + Parser(T, U).new("Flunk") { |_| raise ParserFail.new("Flunked parser") } + end + + # Creates a parser that returns the first token in the input stream. + # Fails if the input stream is empty. + # Analagous to a `.` in a regular expression. + def any_token : Parser(T, T) forall T + Parser(T, T).new("Any Token") do |tokens| + if tokens.empty? + raise ParserFail.new("Expected a token, got EOF") + else + Result.new(tokens[1..], tokens[0]) + end + end + end + + # Creates a parser that succeeds with `nil` if the input stream is empty. + # Fails if the input stream is not empty. + # Analagous to a `$` in a regular expression. + def eof : Parser(T, Nil) forall T + Parser(T, Nil).new("EOF") do |tokens| + if tokens.empty? + Result.new(tokens, nil) + else + raise ParserFail.new("Expected EOF, got a token") + end + end + end + + # Creates a parser that parses the first token in the input stream + # if that token satisfies a given predicate. + # Fails if the input stream is empty or if the predicate fails. + # To test a predicate against any parser result, see `Parser#assert`. + # TODO: the type-checker hates me + def satisfy(f : T -> Bool) : Parser(T, T) forall T + #p = any_token(T).assert(f).named("Satisfy") + Parser(T, T).new("Satisfy") do |tokens| + Basic.any_token.assert(f).parse(tokens) + end + end + + # :ditto: + def satisfy(&block : T -> Bool) : Parser(T, T) forall T + satisfy(T, block) end end end -- cgit v1.2.1