diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/parcom.cr | 56 | ||||
| -rw-r--r-- | src/parcom/basic.cr | 59 |
2 files changed, 110 insertions, 5 deletions
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 |
