go-parsekit/parse/api.go

219 lines
7.9 KiB
Go

package parse
import (
"fmt"
"io"
"git.makaay.nl/mauricem/go-parsekit/tokenize"
)
// API holds the internal state of a parse run and provides an API that
// parse.Handler functions can use to:
//
// • communicate with tokenize.Handler functions (Peek, Accept, ExpectEndOfFile, Result)
//
// • update the parser status (Error, Expected, Stop)
//
// • call other parse.Handler functions, the core of recursive-descent parsing (Handle)
type API struct {
tokenAPI *tokenize.API // the tokenize.API, used for communicating with tokenize.Handler functions
Result *tokenize.Result // a struct, providing access to the results of the last successful Peek() or Accept() call
err error // parse error, retrieved by Error(), using API methods is denied when set
stopped bool // a boolean set to true by Stop()
}
// Peek checks if the upcoming input data matches the provided tokenize.Handler.
// 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.
//
// No results (data + tokens) are returned by Peek(). If want access to the data
// through parse.API.Result, make use of Peek() instead.
func (parseAPI *API) Peek(tokenHandler tokenize.Handler) bool {
tokenAPI := parseAPI.tokenAPI
snap := tokenAPI.MakeSnapshot()
ok := parseAPI.invokeTokenizeHandler("Peek", tokenHandler)
tokenAPI.Result.Store()
tokenAPI.RestoreSnapshot(snap)
return ok
}
// Accept checks if the upcoming input data matches the provided tokenize.Handler.
// If it does, then true will be returned and the read cursor will be moved
// forward to beyond the match that was found. Otherwise false will be
// and the read cursor will stay at the same position.
//
// After calling this method, you can retrieve the results through the API.Result field.
func (parseAPI *API) Accept(tokenHandler tokenize.Handler) bool {
tokenAPI := parseAPI.tokenAPI
ok := parseAPI.invokeTokenizeHandler("Accept", tokenHandler)
if ok {
// Keep track of the results as produced by this child.
tokenAPI.Result.Store()
// Flush the output as initialization for the next token handler.
tokenAPI.Output.Flush()
// Also flush the input reader buffer. Accepting input means that we
// are moving forward in the input file and that already read input
// can therefore be cleared. Doing so saves on memory usage.
tokenAPI.Input.Flush()
}
return ok
}
func (parseAPI *API) Skip(tokenHandler tokenize.Handler) bool {
tokenAPI := parseAPI.tokenAPI
tokenAPI.Output.Suspend()
if !parseAPI.invokeTokenizeHandler("Skip", tokenHandler) {
tokenAPI.Output.Resume()
return false
}
tokenAPI.Output.Resume()
tokenAPI.Result.Clear()
tokenAPI.Input.Flush()
return true
}
// invokeTokenizeHandler forks the tokenize.API, and invokes the tokenize.Handler
// in the context of the created child. The child is returned, so the caller
// has full control over merging and disposing the child.
func (parseAPI *API) invokeTokenizeHandler(name string, tokenHandler tokenize.Handler) bool {
parseAPI.panicWhenStoppedOrInError(name)
if tokenHandler == nil {
callerPanic(name, "parsekit.parse.API.{name}(): {name}() called with nil tokenHandler argument at {caller}")
}
return tokenHandler(parseAPI.tokenAPI)
}
// panicWhenStoppedOrInError will panic when the parser has produced an error
// or when it has been stopped. It is used from the API methods, to
// prevent further calls to the API on these occasions.
//
// Basically, this guard helps with proper coding of parsers, making sure
// that clean routes are followed. You can consider this check a runtime
// unit test.
func (parseAPI *API) panicWhenStoppedOrInError(name string) {
if !parseAPI.IsStoppedOrInError() {
return
}
after := "Error()"
if parseAPI.stopped {
after = "Stop()"
}
callerPanic(name, "parsekit.parse.API.{name}(): Illegal call to {name}() at {caller}: "+
"no calls allowed after API.%s", after)
}
// IsStoppedOrInError checks if the parser has stopped or if an error was set.
// When true, then the parser can no longer continue. If your parser tries to
// call parse.API methods when true is returned, this will result in a panic.
func (parseAPI *API) IsStoppedOrInError() bool {
return parseAPI.stopped || parseAPI.err != nil
}
// Handle executes other parse.Handler functions from within the active
// parse.Handler function.
//
// The boolean return value is true when the parser can still continue.
// It will be false when either an error was set using Error(), or the
// parser was stopped using Stop().
//
// Instead of calling another handler using this method, you can also call
// that other handler directly. However, it is generally advised to make use
// of this method, because it performs some sanity checks and it will return
// an easy to use boolean indicating whether the parser can continue or not.
func (parseAPI *API) Handle(handler Handler) bool {
parseAPI.panicWhenStoppedOrInError("Handle")
parseAPI.panicWhenHandlerNil("Handle", handler)
handler(parseAPI)
if parseAPI.IsStoppedOrInError() {
return false
}
return true
}
func (parseAPI *API) panicWhenHandlerNil(name string, parseHandler Handler) {
if parseHandler == nil {
callerPanic(name, "parsekit.parse.API.{name}(): {name}() called with nil input at {caller}")
}
}
// Stop tells the parser that the parsing process has been completed.
//
// When the initial parse.Handler function returns without stopping first
// and without running into an error, the method ExpectEndOfFile() is automatically
// called to verify if the end of the file was reached. If not, then things will
// end in an unexpected input error.
//
// Note:
// 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 API methods are allowed.
// Calling a method in this state will result in a panic.
func (parseAPI *API) Stop() {
parseAPI.stopped = true
}
// SetError sets the error message in the API.
//
// After setting an error, no more calls to API methods are allowed.
// Calling a method in this state will result in a panic.
// You can still call SetError() though, to set a different error message
// if you feel the need to do so.
func (parseAPI *API) SetError(format string, data ...interface{}) {
message := fmt.Sprintf(format, data...)
parseAPI.err = fmt.Errorf("%s at %s", message, parseAPI.tokenAPI.Input.Cursor())
}
// ExpectEndOfFile checks if the end of the input file has been reached.
//
// When it finds that the end of the file was indeed reached, then the parser
// will be stopped through Stop(). Otherwise, the unexpected input is reported
// using Expected("end of file").
func (parseAPI *API) ExpectEndOfFile() {
parseAPI.panicWhenStoppedOrInError("ExpectEndofFile")
if parseAPI.Peek(tokenize.A.EndOfFile) {
parseAPI.Stop()
} else {
parseAPI.Expected("end of file")
}
}
// Expected sets a parser error that indicates that some unexpected
// input was encountered.
//
// The 'expected' argument can be an empty string. In that case the error
// message will not contain a description of the expected input.
//
// This method automatically produces an error message for a couple of situations:
//
// • the input simply didn't match the expectation
//
// • the end of the input was reached
//
// • there was an error while reading the input.
func (parseAPI *API) Expected(expected string) {
parseAPI.panicWhenStoppedOrInError("Expected")
_, err := parseAPI.tokenAPI.Input.Byte.Peek(0)
switch {
case err == nil:
parseAPI.SetError("unexpected input%s", fmtExpects(expected))
case err == io.EOF:
parseAPI.SetError("unexpected end of file%s", fmtExpects(expected))
default:
parseAPI.SetError("unexpected error '%s'%s", err, fmtExpects(expected))
}
}
func fmtExpects(expected string) string {
if expected == "" {
return ""
}
return fmt.Sprintf(" (expected %s)", expected)
}