# parcom A simple parser combinator library with a dumb name. ## WARNING This library is a work in progress. Any version of this library <1.0.0 should not be used in production environments. The library is still growing and breaking changes may occur at any time. ## Description Parcom is a Crystal library the provides parser combinator functionality. ## Prerequisites * Git ## Installation Add the following dependency to your project's `shard.yml` file: ``` dependencies: parcom: git: "https://git.matthewhall.xyz/parcom" version: "0.3.0" ``` Then, run ``` shards install ``` ## General usage Parcom parsers work by creating parser objects, and then calling their `#parse` method with the given input. As this library use parser combinators, complex parser objects should be made by combining simple parsers together. ## Example walkthrough Before we get started, it is recommended to `include` the Parcom module in whatever namespace you are working in: ``` require "parcom" include Parcom module YourModule def self.main puts "Hello world!" end end YourModule.main ``` Suppose we want to parse a `Hash(Int32, Int32)` literal from a string. First, we should define how to parse a digit: ``` # This defines a parser that will parse a single Char, check if # it is a digit, and fail if it is not a digit. d = Parser(Char, Char).satisfy(&.number?) ``` Numbers often have one or more digits [citation needed], so let's make another parser based on `d` that parses multiple digits: ``` # `Parser#some` is a method that creates a new parser that parses # one or more instances of what the original parser would parse. abs_num = d.some ``` We're not quite done with this yet, as we want a parser of `Int32`, but this parser will parse an `Array(Char)`. We need to change the value inside the parser with the `Parser#map` method: ``` # The `Parser#map` method accepts a block or proc that takes the expected # parser result and transforms it into something else. # In this case, we're converting our array of digits into an Int32. abs_num = d.some.map { |ds| ds.join.to_i32 } ``` Now we have a parser that can parse positive integers (in base 10). But what about negative numbers? First, we make a parser that parses a '-' sign if it can, but doesn't fail if it can't fine one: ``` # `Parser#optional` creates a new parser that tries to parse with the original # parser, but will return `nil` without consuming any input instead of failing. sign = Parser.token('-').optional ``` Then we can change the value to `1` or `-1` to multiply by later, based on the result: ``` sign = Parser.token('-').optional.map do |minus_or_nil| minus_or_nil.nil? : -1_i32 : 1_i32 end ``` Another way to do this is to use `Parser#recover`, which allows a default value to be specified: ``` # `#map_const` is like `#map`, but it takes a single value to replace # the parser value with unconditionally. sign = Parser.token('-').map_const(-1_i32).recover(1_i32) ``` Final code: ``` d = Parser(Char, Char).satisfy(&.number?) abs_num = d.some.map { |ds| ds.join.to_i32 } sign = Parser.token('-').map_const(-1_i32).recover(1_i32) int32 = parser_chain Char, Int32, "int32", {s, sign}, {n, abs_num}, finally: Parser.pure(n * s) ```