go-parsekit/parseapi.go

217 lines
7.4 KiB
Go

package parsekit
import (
"fmt"
"io"
"git.makaay.nl/mauricem/go-parsekit/common"
"git.makaay.nl/mauricem/go-parsekit/tokenize"
)
// ParseAPI holds the internal state of a parse run and provides an API that
// ParseHandler methods can use to communicate with the parser.
type ParseAPI struct {
tokenAPI *tokenize.TokenAPI // the TokenAPI, used for communicating with TokenHandler functions
loopCheck map[string]bool // used for parser loop detection
result *tokenize.TokenHandlerResult // Last TokenHandler result as produced by On(...).Accept()
err *common.Error // error during parsing, retrieved by Error(), further ParseAPI calls are ignored
stopped bool // a boolean set to true by Stop(), further ParseAPI calls are ignored
}
// Peek checks if the upcoming input data matches the provided TokenHandler.
// If it does, then true will be returned, false otherwise. The read cursor
// will be kept at the same position, so the next call to Peek() or Accept()
// will start from the same cursor position.
//
// After calling this method, you can retrieve the produced TokenHandlerResult
// using the ParseAPI.Result() method.
func (p *ParseAPI) Peek(tokenHandler tokenize.TokenHandler) bool {
p.result = nil
forkedTokenAPI, ok := p.invokeTokenHandler("Peek", tokenHandler)
if ok {
p.result = forkedTokenAPI.Result()
p.tokenAPI.ClearResults()
p.tokenAPI.DetachChilds()
}
return ok
}
// Accept checks if the upcoming input data matches the provided TokenHandler.
// If it does, then true will be returned, false otherwise. The read cursor
// will be moved forward to beyond the match that was found.
//
// After calling this method, you can retrieve the produced TokenHandlerResult
// using the ParseAPI.Result() method.
func (p *ParseAPI) Accept(tokenHandler tokenize.TokenHandler) bool {
p.result = nil
forkedTokenAPI, ok := p.invokeTokenHandler("Accept", tokenHandler)
if ok {
forkedTokenAPI.Merge()
p.result = p.tokenAPI.Result()
p.tokenAPI.DetachChilds()
if p.tokenAPI.FlushReader() {
p.initLoopCheck()
}
}
return ok
}
func (p *ParseAPI) invokeTokenHandler(name string, tokenHandler tokenize.TokenHandler) (*tokenize.TokenAPI, bool) {
p.panicWhenStoppedOrInError()
p.checkForLoops()
if tokenHandler == nil {
common.CallerPanic(2, "parsekit.ParseAPI.%s(): %s() called with nil tokenHandler argument at {caller}", name, name)
}
p.result = nil
p.tokenAPI.ClearResults()
child := p.tokenAPI.Fork()
ok := tokenHandler(child)
return child, ok
}
// panicWhenStoppedOrInError will panic when the parser has produced an error
// or when it has been stopped. It is used from the ParseAPI methods, to
// prevent further calls to the ParseAPI on these occasions.
//
// Basically, this guard ensures proper coding of parsers, making sure
// that clean routes are followed. You can consider this check a runtime
// unit test.
func (p *ParseAPI) panicWhenStoppedOrInError() {
if !p.isStoppedOrInError() {
return
}
called := common.CallerFunc(1)
after := "Error()"
if p.stopped {
after = "Stop()"
}
common.CallerPanic(2, "parsekit.ParseAPI.%s(): Illegal call to %s() at {caller}: "+
"no calls allowed after ParseAPI.%s", called, called, after)
}
func (p *ParseAPI) isStoppedOrInError() bool {
return p.stopped || p.err != nil
}
func (p *ParseAPI) initLoopCheck() {
p.loopCheck = map[string]bool{}
}
func (p *ParseAPI) checkForLoops() {
filepos := common.CallerFilePos(3)
if _, ok := p.loopCheck[filepos]; ok {
common.CallerPanic(3, "parsekit.ParseAPI: Loop detected in parser at {caller}")
}
p.loopCheck[filepos] = true
}
// Result returns a TokenHandlerResult struct, containing results as produced by the
// last Peek() or Accept() call.
//
// When Result() is called without first doing a Peek() or Accept(), then no
// result will be available and the method will panic.
func (p *ParseAPI) Result() *tokenize.TokenHandlerResult {
result := p.result
if p.result == nil {
common.CallerPanic(1, "parsekit.ParseAPI.TokenHandlerResult(): TokenHandlerResult() called "+
"at {caller} without calling ParseAPI.Peek() or ParseAPI.Accept() on beforehand")
}
return result
}
// Handle is used to execute other ParseHandler functions from within your
// ParseHandler function.
//
// The boolean return value is true when the parser can still continue.
// It will be false when either an error was set (using ParseAPI.Error()),
// or the parser was stopped (using ParseAPI.Stop()).
func (p *ParseAPI) Handle(parseHandler ParseHandler) bool {
p.panicWhenStoppedOrInError()
p.panicWhenParseHandlerNil(parseHandler)
parseHandler(p)
return !p.isStoppedOrInError()
}
func (p *ParseAPI) panicWhenParseHandlerNil(parseHandler ParseHandler) {
if parseHandler == nil {
common.CallerPanic(2, "parsekit.ParseAPI.Handle(): Handle() called with nil input at {caller}")
}
}
// Stop is used by the parser impementation to tell the ParseAPI that it has
// completed the parsing process successfully.
//
// When the parser implementation returns without stopping first (and
// without running into an error), the Parser.Execute() will call
// ParserAPI.ExpectEndOfFile() to check if the end of the file was reached.
// If not, then things will end in an unexpected input error.
// Even though this fallback mechanism will work in a lot of cases, try to make
// your parser explicit about things and call Stop() actively yourself.
//
// After stopping, no more calls to ParseAPI methods are allowed.
// Calling a method in this state will result in a panic.
func (p *ParseAPI) Stop() {
p.stopped = true
}
// Error sets the error message in the ParseAPI.
//
// After setting an error, no more calls to ParseAPI methods are allowed.
// Calling a method in this state will result in a panic.
func (p *ParseAPI) Error(format string, args ...interface{}) {
// No call to p.panicWhenStoppedOrInError(), to allow a parser to
// set a different error message when needed.
message := fmt.Sprintf(format, args...)
p.err = &common.Error{message, *p.tokenAPI.Result().Cursor()}
}
// ExpectEndOfFile can be used to check if the input is at end of file.
//
// When it finds that the end of the file was indeed reached, then the
// parser will be stopped through ParseAPI.Stop(). Otherwise unexpected
// input is reported through ParseAPI.Expected() with "end of file"
// as the expectation.
func (p *ParseAPI) ExpectEndOfFile() {
p.panicWhenStoppedOrInError()
if p.Peek(tokenize.A.EndOfFile) {
p.Stop()
} else {
p.Expected("end of file")
}
}
// Expected is used to set an error that tells the user that some
// unexpected input was encountered, and what input was expected.
//
// The 'expected' argument can be an empty string. In that case the error
// message will not contain a description of the expected input.
//
// It automatically produces an error message for a couple of situations:
// 1) the input simply didn't match the expectation
// 2) the end of the input was reached
// 3) there was an error while reading the input.
func (p *ParseAPI) Expected(expected string) {
p.panicWhenStoppedOrInError()
_, err := p.tokenAPI.NextRune()
switch {
case err == nil:
p.Error("unexpected input%s", fmtExpects(expected))
case err == io.EOF:
p.Error("unexpected end of file%s", fmtExpects(expected))
default:
p.Error("unexpected error '%s'%s", err, fmtExpects(expected))
}
}
func fmtExpects(expected string) string {
if expected == "" {
return ""
}
return fmt.Sprintf(" (expected %s)", expected)
}