require "./parcom/*" module Parcom VERSION = "0.1.0" # A ParserFail exception should be raised by `Parser#parse` when # a parse attempt is unsuccessful. class ParserFail < Exception end # `Tokens` is an `Array` wrapper struct to store the input # stream of one or more `Parser` objects. # A `Tokens` can be created from any `Iterable`, along with # `String` objects using a special constructor. struct Tokens(T) getter tokens # Constructs a `Tokens(Char)` from a `String`. def self.from_string(s : String) : Tokens(Char) Tokens.new(s.chars) end # Constructs a `Tokens` from an `Iterable`. def initialize(ts : Iterable(T)) if ts.responds_to?(:to_a) @tokens = ts.to_a else @tokens = [] of T ts.each { |t| @tokens << t } end end # Exposes `Array#[](Int)`. def [](index : Int) : T @tokens[index] end # Exposes `Array#[](Int, Int)`, but wraps the returned array in a new `Tokens`. def [](start : Int, count : Int) : Tokens(T) Tokens.new(@tokens[start, count]) end # Exposes `Array#[](Range)`, but wraps the returned array in a new `Tokens`. def [](range : Range) : Tokens(T) Tokens.new(@tokens[range]) end # Like `#[]`, but returns `nil` instead of raising an `IndexError`. def []?(*args) self.[](*args) rescue IndexError nil end # Exposes `Array#empty?`. def empty? : Bool @tokens.empty? end end # A `Result` stores a `Tokens` object and a parsed value, # and is effectively used to store the state of a parser chain. # This is used instead of a `Tuple` or `NamedTuple` because: # 1. This is more idiomatic than a `Tuple`. # 2. Crystal does not support generic named tuples. struct Result(T, U) getter tokens, value 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 # 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