diff options
| author | Matthew Hall <hallmatthew314@gmail.com> | 2023-03-17 22:50:14 +1300 |
|---|---|---|
| committer | Matthew Hall <hallmatthew314@gmail.com> | 2023-03-17 22:50:14 +1300 |
| commit | 982d239252b54297ff3558894038871f8d3a4175 (patch) | |
| tree | 41ab6073fd601caf2da0bf87c4cd8226b87b7126 | |
| parent | f023ca56dbf9372464afe0060270fcef85271db0 (diff) | |
Please work this time
| -rw-r--r-- | spec/parcom_spec.cr | 65 | ||||
| -rw-r--r-- | src/parcom.cr | 83 | ||||
| -rw-r--r-- | src/parcom/basic.cr | 65 | ||||
| -rw-r--r-- | src/parcom/parser.cr | 137 |
4 files changed, 169 insertions, 181 deletions
diff --git a/spec/parcom_spec.cr b/spec/parcom_spec.cr index ceb7de8..05fd12d 100644 --- a/spec/parcom_spec.cr +++ b/spec/parcom_spec.cr @@ -79,29 +79,10 @@ describe Result do 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 +describe Parser do + describe "Parser.pure" do v = 'a' - p = Basic(Char, Char).pure(v) + p = Parser(Char, Char).pure(v) tokens = Tokens.from_string("____") result = p.parse(tokens) @@ -114,15 +95,15 @@ describe Basic do end end - describe "Basic.flunk" do - p = Basic(Char, Char).flunk + describe "Parser.flunk" do + p = Parser(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 + describe "Parser.any_token" do + p = Parser(Char, Nil).any_token it "parses the first token in the input stream" do tokens = Tokens.from_string("abcd") @@ -138,8 +119,8 @@ describe Basic do end end - describe "Basic.eof" do - p = Basic(Char, Nil).eof + describe "Parser.eof" do + p = Parser(Char, Nil).eof it "succeeds with nil if the input stream is empty" do tokens = Tokens.from_string("") @@ -155,14 +136,12 @@ describe Basic do 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' }) + describe "Parser.satisfy" do + p = Parser(Char, Char).satisfy { |c| 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 = p.parse(tokens) result.value.should eq(tokens[0]) result.tokens.should eq(tokens[1..]) @@ -173,5 +152,25 @@ describe Basic do expect_raises(ParserFail) { p.parse(tokens) } end end + + describe "#assert" do + p = Parser(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 +#pending Basic do +##end + diff --git a/src/parcom.cr b/src/parcom.cr index ffa5654..8387752 100644 --- a/src/parcom.cr +++ b/src/parcom.cr @@ -77,88 +77,5 @@ module Parcom map(block) end end - - # 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)) - end - - def initialize(@name : String, &block : Tokens(T) -> Result(T, U)) - @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 - end - - def parse(tokens : Tokens(T)) : Result(T, U) - @f.call(tokens) - end - - def parse?(tokens : Tokens(T)) : Result(T, U)? - parse(tokens) - rescue - nil - end - - def assert(f : U -> Bool) : Parser(T, U) - p = self - 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}") - end - result - end - end - - 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 deleted file mode 100644 index 4f01021..0000000 --- a/src/parcom/basic.cr +++ /dev/null @@ -1,65 +0,0 @@ -require "../parcom.cr" - -module Parcom - 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(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 - diff --git a/src/parcom/parser.cr b/src/parcom/parser.cr new file mode 100644 index 0000000..adf0bba --- /dev/null +++ b/src/parcom/parser.cr @@ -0,0 +1,137 @@ +require "../parcom.cr" + +module Parcom + # 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 + + # Creates a parser that always succeeds with the given value. + # This parser consumes no input. + def self.pure(value : U) : Parser(T, U) + Parser(T, U).new("Pure #{value}") { |tokens| Result.new(tokens, value) } + end + + # Creates a parser that always fails. + def self.flunk : Parser(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 self.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 self.eof : Parser(T, Nil) + 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`. + def self.satisfy(f : T -> Bool) : Parser(T, T) + Parser(T, T).any_token.assert(f).named("Satisfy") + end + + # :ditto: + def self.satisfy(&block : T -> Bool) : Parser(T, T) + satisfy(block) + end + + def initialize(@name : String, @f : Tokens(T) -> Result(T, U)) + end + + def initialize(@name : String, &block : Tokens(T) -> Result(T, U)) + @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 + end + + def parse(tokens : Tokens(T)) : Result(T, U) + @f.call(tokens) + end + + def parse?(tokens : Tokens(T)) : Result(T, U)? + parse(tokens) + rescue + nil + end + + def assert(f : U -> Bool) : Parser(T, U) + p = self + Parser(T, U).new("#{p.name} (assertion)") do |tokens| + result = p.parse(tokens) + unless f.call(result.value) + raise ParserFail.new("Assertion failed for value #{result.value}") + end + result + end + end + + 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 + |
