libchess/pkg/pgn/parser.go
2020-05-08 22:55:36 +02:00

242 lines
4.0 KiB
Go

package pgn
import (
"bufio"
"fmt"
"strings"
"code.c-base.org/gochess/libchess/pkg/game"
)
// PoolParsers defines how may parsers are prenitialized.
const PoolParsers = 8
// ParseFn defines the signature of a parser function.
type ParseFn func(*Parser) ParseFn
// Parser implements a PGN parser.
type Parser struct {
lexer *Lexer
errors chan error
games chan *Game
game *Game
tokenBuf *Token
useBuf bool
}
var parserFactory = make(chan *Parser, PoolParsers)
func init() {
go func() {
for {
parserFactory <- &Parser{
errors: make(chan error),
games: make(chan *Game),
game: newGame(),
}
}
}()
}
// NewParser returns an initialized parser
func NewParser(input *bufio.Reader) *Parser {
p := <-parserFactory
p.lexer = NewLexer(input)
go p.run()
return p
}
func (p *Parser) run() {
defer close(p.errors)
defer close(p.games)
for fn := parseTagSection; fn != nil; {
fn = fn(p)
}
}
// Next returns the next parsed game from the input stream or an error.
func (p *Parser) Next() (*Game, error) {
select {
case err := <-p.errors:
return nil, err
case g := <-p.games:
return g, nil
}
}
func (p *Parser) next() (*Token, error) {
if p.useBuf {
p.useBuf = false
return p.tokenBuf, nil
}
t, err := p.lexer.Next()
if err != nil {
return nil, err
}
p.tokenBuf = t
return t, nil
}
func (p *Parser) undo() {
p.useBuf = true
}
func (p *Parser) throwUnexpected(t *Token) {
p.errors <- fmt.Errorf(
"parsing error: unexpected token in line %d at %d: %q",
t.Line,
t.Col,
t.Value,
)
}
func throwUnexpectedEOF(p *Parser) ParseFn {
p.errors <- fmt.Errorf(
"parsing error: unexpected EOF",
)
return nil
}
func (p *Parser) emit() {
p.games <- p.game
p.game = newGame()
}
func parseTagSection(p *Parser) ParseFn {
for {
// grab next token
t, err := p.next()
// bail out on error
if err != nil {
p.errors <- err
return nil
}
// handle for EOF
if t == nil || t.Type == TokenEOF {
p.emit()
return nil
}
switch t.Type {
case TokenNewline, TokenWhitespace:
// noop
case TokenBracketLeft:
return parseTag
case TokenSymbol:
p.undo()
return parseMovetext
default:
p.throwUnexpected(t)
return nil
}
}
}
func parseTag(p *Parser) ParseFn {
tag := game.Tag{}
findSymbol:
for {
t, err := p.next()
if err != nil {
p.errors <- err
return nil
}
if t == nil || t.Type == TokenEOF {
return throwUnexpectedEOF
}
switch t.Type {
case TokenNewline, TokenWhitespace, TokenComment:
// noop
case TokenSymbol:
tag.Key = t.Value
break findSymbol
default:
p.throwUnexpected(t)
return nil
}
}
findValue:
for {
t, err := p.next()
if err != nil {
p.errors <- err
return nil
}
if t == nil || t.Type == TokenEOF {
return throwUnexpectedEOF
}
switch t.Type {
case TokenNewline, TokenWhitespace, TokenComment:
// noop
case TokenString:
tag.Value = t.Value
break findValue
default:
p.throwUnexpected(t)
return nil
}
}
for {
t, err := p.next()
if err != nil {
p.errors <- err
return nil
}
if t == nil || t.Type == TokenEOF {
return throwUnexpectedEOF
}
switch t.Type {
case TokenNewline, TokenWhitespace, TokenComment:
// noop
case TokenBracketRight:
p.game.Tags = append(p.game.Tags, tag)
return parseTagSection
default:
p.throwUnexpected(t)
return nil
}
}
}
func parseMovetext(p *Parser) ParseFn {
isTermination := func(s string) bool {
switch s {
case "0-1", "1-0", "1/2", "1/2-1/2", "*":
return true
default:
return false
}
}
for {
t, err := p.next()
if err != nil {
p.errors <- err
return nil
}
if t == nil || t.Type == TokenEOF {
p.undo()
return parseTagSection
}
switch t.Type {
case TokenNewline, TokenWhitespace, TokenComment:
// noop
case TokenSymbol:
if strings.Contains(t.Value, ".") {
continue
}
if !isTermination(t.Value) {
p.game.Moves = append(p.game.Moves, t.Value)
}
case TokenBracketLeft:
p.emit()
p.undo()
return parseTagSection
default:
p.throwUnexpected(t)
return nil
}
}
}