diff options
| author | Matthew Hall <hallmatthew314@gmail.com> | 2023-03-17 23:32:54 +1300 |
|---|---|---|
| committer | Matthew Hall <hallmatthew314@gmail.com> | 2023-03-17 23:32:54 +1300 |
| commit | 72236e9bcaf7bff4a2bfbeb50ddf5d0261f74927 (patch) | |
| tree | 231d0b059e1a19c8b096395d88d14b069711b763 | |
| parent | 982d239252b54297ff3558894038871f8d3a4175 (diff) | |
So far so good
| -rw-r--r-- | spec/parcom_spec.cr | 83 | ||||
| -rw-r--r-- | src/parcom/parser.cr | 44 |
2 files changed, 117 insertions, 10 deletions
diff --git a/spec/parcom_spec.cr b/spec/parcom_spec.cr index 05fd12d..14e232e 100644 --- a/spec/parcom_spec.cr +++ b/spec/parcom_spec.cr @@ -80,7 +80,7 @@ describe Result do end describe Parser do - describe "Parser.pure" do + describe "self.pure" do v = 'a' p = Parser(Char, Char).pure(v) tokens = Tokens.from_string("____") @@ -95,14 +95,14 @@ describe Parser do end end - describe "Parser.flunk" do + describe "self.flunk" do p = Parser(Char, Char).flunk it "always fails" do expect_raises(ParserFail) { p.parse(Tokens.from_string("arbitrary")) } end end - describe "Parser.any_token" do + describe "self.any_token" do p = Parser(Char, Nil).any_token it "parses the first token in the input stream" do @@ -119,7 +119,7 @@ describe Parser do end end - describe "Parser.eof" do + describe "self.eof" do p = Parser(Char, Nil).eof it "succeeds with nil if the input stream is empty" do @@ -136,7 +136,7 @@ describe Parser do end end - describe "Parser.satisfy" do + describe "self.satisfy" do p = Parser(Char, Char).satisfy { |c| c == 'a' } it "succeeds if the token passes the predicate" do @@ -153,6 +153,23 @@ describe Parser do end end + describe "self.token" do + a = Parser(Char, Char).token('a') + + it "succeeds with the given token if it is at the start" do + tokens = Tokens.from_string("abc") + result = a.parse(tokens) + + result.value.should eq('a') + result.tokens.should eq(tokens[1..]) + end + + it "fails if the given token is not present" do + tokens = Tokens.from_string("bbc") + expect_raises(ParserFail) { a.parse(tokens) } + end + end + describe "#assert" do p = Parser(Char, Char).any_token.assert { |c| c == 'a' } @@ -169,8 +186,58 @@ describe Parser do expect_raises(ParserFail) { p.parse(tokens) } end end -end -#pending Basic do -##end + describe "#map" do + p = Parser(Char, Char).any_token + p_mapped = p.map { |c| c.ord } + tokens = Tokens.from_string("abcd") + + normal_result = p.parse(tokens) + mapped_result = p_mapped.parse(tokens) + + it "parses the same amount from the input" do + mapped_result.tokens.should eq(normal_result.tokens) + end + + it "changes the value of the result" do + mapped_result.value.should eq('a'.ord) + end + end + + describe "#|" do + a = Parser(Char, Char).token('a') + b = Parser(Char, Char).token('b') + p = a | b + + it "succeeds with the first value if both parsers succeed" do + p2 = a | Parser(Char, Char).pure('z') + tokens = Tokens.from_string("abcd") + result = p2.parse(tokens) + + result.value.should eq('a') + result.tokens.should eq(tokens[1..]) + end + + it "succeeds with the first value if only the first parser succeeds" do + tokens = Tokens.from_string("abcd") + result = p.parse(tokens) + + result.value.should eq('a') + result.tokens.should eq(tokens[1..]) + end + + it "succeeds with the second value if only the second parser succeeds" do + tokens = Tokens.from_string("bbcd") + result = p.parse(tokens) + + result.value.should eq('b') + result.tokens.should eq(tokens[1..]) + end + + it "fails if both parsers fail" do + tokens = Tokens.from_string("cbcd") + expect_raises(ParserFail) { p.parse(tokens) } + end + end +end diff --git a/src/parcom/parser.cr b/src/parcom/parser.cr index adf0bba..3098080 100644 --- a/src/parcom/parser.cr +++ b/src/parcom/parser.cr @@ -80,9 +80,19 @@ module Parcom satisfy(block) end + # Creates a parser that parses the fist token in the input stream + # if that token matches the provided token. + def self.token(token : T) : Parser(T, T) + Parser(T, T).satisfy { |x| x == token }.named("Token <#{token}>") + end + + # Creates a new parser from a `Proc`. + # The `Proc` should have the properties outlined above. def initialize(@name : String, @f : Tokens(T) -> Result(T, U)) end + # Creates a new parser from a block. + # The block should have the properties outline above. def initialize(@name : String, &block : Tokens(T) -> Result(T, U)) @f = block end @@ -97,16 +107,24 @@ module Parcom self end + # Tries to parse some kind of data from the given input stream. + # This method calls the `Proc` or block this parser object was + # initialized with. def parse(tokens : Tokens(T)) : Result(T, U) @f.call(tokens) end + # Same as `#parse(Tokens(T)) : Result(T, U)`, but returns `nil` + # instead of raising an exception if parsing fails. def parse?(tokens : Tokens(T)) : Result(T, U)? parse(tokens) rescue nil end + # Creates a new parser that is the same as the parser object it is + # called from, but tests the result against a given predicate. + # If the value does not pass the test, the parser fails. def assert(f : U -> Bool) : Parser(T, U) p = self Parser(T, U).new("#{p.name} (assertion)") do |tokens| @@ -118,20 +136,42 @@ module Parcom end end + # :ditto: def assert(&block : U -> Bool) : Parser(T, U) assert(block) end - def map(f : U -> T) : Parser(T, V) forall V + # Creates a new parser that is the same as the parser object it is + # called from, but transforms the result into something else via a + # given function. + # The function in question should not introduce side effects. + def map(f : U -> V) : 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 + # :ditto: + def map(&block : U -> V) : Parser(T, V) forall V map(block) end + + def |(p2 : Parser(T, U)) : Parser(T, U) + p1 = self + Parser(T, U).new("#{p1.name} or #{p2.name}") do |tokens| + p1.parse(tokens) + rescue ParserFail + p2.parse(tokens) + end + end + + # Creates a new parser that is the same as the parser object it is + # called from, but it will return a default value without consuming + # any input instead of failing. + #def recover(default : U) : Parser(T, U) + # nil + #end end end |
