aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/parcom.cr56
-rw-r--r--src/parcom/basic.cr59
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