Created
January 24, 2022 20:03
-
-
Save asterite/7584e3cc1ed43bd553741772907e0d7c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require "benchmark" | |
module Parser(T) | |
def self.char(a : Char) | |
CharParser.new(a) | |
end | |
def self.int | |
IntParser.new | |
end | |
def self.skip_whitespace | |
SkipWhitespace.new | |
end | |
def self.combine(&block : ParserCombinator -> T) forall T | |
Combinator(T).new(block) | |
end | |
abstract def parse(reader : Char::Reader) : Success(T) | Failure | |
struct Combinator(T) | |
include Parser(T) | |
def initialize(@block : ParserCombinator -> T) | |
@combinator = ParserCombinator.new | |
end | |
def parse(reader : Char::Reader) : Success(T) | Failure | |
@combinator.reader = reader | |
value = @block.call(@combinator) | |
succeed value, @combinator.reader | |
end | |
end | |
class ParserCombinator | |
property! reader : Char::Reader | |
def parse(parser : Parser(T)) : T forall T | |
result = parser.parse(reader) | |
case result | |
in Success | |
@reader = result.reader | |
result.value | |
in Failure | |
raise "Failed to parse at #{result.reader.current_char.inspect}" | |
end | |
end | |
end | |
def parse(string : String) | |
reader = Char::Reader.new(string) | |
result = parse(reader) | |
case result | |
in Success | |
result.value | |
in Failure | |
raise "Failed to parse at #{result.reader.current_char.inspect}" | |
end | |
end | |
def succeed(value : T, reader : Char::Reader) | |
Success(T).new(value, reader) | |
end | |
def fail(reader : Char::Reader) | |
Failure.new(reader) | |
end | |
record Success(T), value : T, reader : Char::Reader | |
record Failure, reader : Char::Reader | |
struct CharParser | |
include Parser(Char) | |
def initialize(@char : Char) | |
end | |
def parse(reader : Char::Reader) : Success(Char) | Failure | |
if reader.current_char == @char | |
reader.next_char | |
succeed @char, reader | |
else | |
fail reader | |
end | |
end | |
end | |
struct IntParser | |
include Parser(Int32) | |
def parse(reader : Char::Reader) : Success(Int32) | Failure | |
value = 0 | |
if reader.current_char == '0' | |
reader.next_char | |
succeed value, reader | |
elsif '0' <= reader.current_char <= '9' | |
while '0' <= reader.current_char <= '9' | |
value *= 10 | |
value += reader.current_char.to_i | |
reader.next_char | |
end | |
succeed value, reader | |
else | |
fail reader | |
end | |
end | |
end | |
struct SkipWhitespace | |
include Parser(Nil) | |
def parse(reader : Char::Reader) : Success(Nil) | Failure | |
while reader.current_char.whitespace? | |
reader.next_char | |
end | |
succeed nil, reader | |
end | |
end | |
end | |
int = Parser.int | |
mul = Parser.combine do |c| | |
a = c.parse(int) | |
c.parse(Parser.skip_whitespace) | |
c.parse(Parser.char('*')) | |
c.parse(Parser.skip_whitespace) | |
b = c.parse(int) | |
a * b | |
end | |
plus = Parser.combine do |c| | |
a = c.parse(mul) | |
c.parse(Parser.skip_whitespace) | |
c.parse(Parser.char('+')) | |
c.parse(Parser.skip_whitespace) | |
b = c.parse(mul) | |
a + b | |
end | |
calc = Parser.combine do |c| | |
a = c.parse(plus) | |
a | |
end | |
string = "3 * 4 + 5 * 6" | |
p = P.new | |
p! calc.parse(string) | |
p! p.parse(string) | |
Benchmark.ips do |x| | |
x.report("parser combinator") do | |
calc.parse(string) | |
end | |
x.report("manual") do | |
p.parse(string) | |
end | |
end | |
class P | |
getter reader : Char::Reader | |
def initialize | |
@reader = Char::Reader.new("") | |
end | |
def parse(string : String) : Int32 | |
@reader = Char::Reader.new(string) | |
parse_plus | |
end | |
def parse_plus | |
a = parse_mul | |
skip_whitespace | |
check '+' | |
skip_whitespace | |
b = parse_mul | |
a + b | |
end | |
def parse_mul | |
a = parse_number | |
skip_whitespace | |
check '*' | |
skip_whitespace | |
b = parse_number | |
a * b | |
end | |
def parse_number | |
value = 0 | |
if reader.current_char == '0' | |
reader.next_char | |
0 | |
elsif '0' <= reader.current_char <= '9' | |
while '0' <= reader.current_char <= '9' | |
value *= 10 | |
value += reader.current_char.to_i | |
reader.next_char | |
end | |
value | |
else | |
raise "Failed to parse at #{reader.current_char.inspect}" | |
end | |
end | |
def check(char : Char) : Nil | |
if reader.current_char == char | |
reader.next_char | |
else | |
raise "Failed to parse at #{reader.current_char.inspect}" | |
end | |
end | |
def skip_whitespace | |
while reader.current_char.whitespace? | |
reader.next_char | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment