go-parsekit/parseapi.go

315 lines
10 KiB
Go

package parsekit
import (
"fmt"
"io"
"strings"
)
// ParseAPI holds the internal state of a parse run and provides an API to
// ParseHandler methods to communicate with the parser.
type ParseAPI struct {
tokenAPI *TokenAPI // the input reader
loopCheck map[string]bool // used for parser loop detection
expecting string // a description of what the current state expects to find (see Expects())
result *TokenResult // Last TokenHandler result as retrieved by On(...).Accept()
err *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
}
// 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, _ := getCaller(1)
parts := strings.Split(called, ".")
calledShort := parts[len(parts)-1]
_, filepos := getCaller(2)
after := "Error()"
if p.stopped {
after = "Stop()"
}
panic(fmt.Sprintf(
"parsekit.ParseAPI.%s(): Illegal call to %s() at %s: "+
"no calls allowed after ParseAPI.%s",
calledShort, calledShort, filepos, 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 := getCaller(2)
if _, ok := p.loopCheck[filepos]; ok {
panic(fmt.Sprintf("parsekit.ParseAPI: Loop detected in parser at %s", filepos))
}
p.loopCheck[filepos] = true
}
// On checks if the input at the current cursor position matches the provided
// TokenHandler. On must be chained with another method that tells the parser
// what action to perform when a match was found:
//
// 1) On(...).Skip() - Only move cursor forward, ignore the matched runes.
//
// 2) On(...).Accept() - Move cursor forward, add runes to parsers's string buffer.
//
// 3) On(...).Stay() - Do nothing, the cursor stays at the same position.
//
// So an example chain could look like this:
//
// p.On(parsekit.A.Whitespace).Skip()
//
// The chain as a whole returns a boolean that indicates whether or not at match
// was found. When no match was found, false is returned and Skip() and Accept()
// will have no effect. Because of this, typical use of an On() chain is as
// expression for a conditional statement (if, switch/case, for). E.g.:
//
// // Skip multiple exclamation marks.
// for p.On(parsekit.A.Excl).Skip() { }
//
// // Fork a route based on the input.
// switch {
// case p.On(parsekit.A.Excl).Stay()
// p.RouteTo(stateHandlerA)
// case p.On(parsekit.A.Colon).Stay():
// p.RouteTo(stateHandlerB)
// default:
// p.RouteTo(stateHandlerC)
// }
//
// // When there's a "hi" on input, then say hello.
// if p.On(parsekit.C.Str("hi")).Accept() {
// fmt.Println("Hello!")
// }
func (p *ParseAPI) On(tokenHandler TokenHandler) *ParseAPIOnAction {
p.panicWhenStoppedOrInError()
p.checkForLoops()
if tokenHandler == nil {
_, filepos := getCaller(1)
panic(fmt.Sprintf(
"parsekit.ParseAPI.On(): On() called with nil "+
"tokenHandler argument at %s", filepos))
}
p.result = nil
p.tokenAPI.result = newTokenResult()
fork := p.tokenAPI.Fork()
ok := tokenHandler(fork)
return &ParseAPIOnAction{
parseAPI: p,
tokenAPI: fork,
ok: ok,
}
}
// ParseAPIOnAction is a struct that is used for building the On()-method chain.
// The On() method will return an initialized struct of this type.
type ParseAPIOnAction struct {
parseAPI *ParseAPI
tokenAPI *TokenAPI
ok bool
}
// Accept tells the parser to move the cursor past a match that was found,
// and to make the TokenResult from the TokenAPI available in the ParseAPI
// through the Result() method.
//
// Returns true in case a match was found.
// When no match was found, then no action is taken and false is returned.
func (a *ParseAPIOnAction) Accept() bool {
if a.ok {
a.tokenAPI.Merge()
a.parseAPI.result = a.tokenAPI.root.result
a.flushTokenAPI()
a.flushReader() //a.flush()
}
return a.ok
}
// Skip tells the parser to move the cursor past a match that was found,
// without making the results available through the ParseAPI.
//
// Note that functionally, you could call Accept() just as well, simply
// ignoring the results. However, the Skip() call is a bit more efficient
// than the Accept() call and (more important if you ask me) the code
// expresses more clearly that your intent is to skip the match.
//
// Returns true in case a match was found.
// When no match was found, then no action is taken and false is returned.
func (a *ParseAPIOnAction) Skip() bool {
if a.ok {
a.tokenAPI.root.cursor = a.tokenAPI.cursor
a.parseAPI.result = nil
a.flushTokenAPI()
a.flushReader()
}
return a.ok
}
// Stay tells the parser to not move the cursor after finding a match.
//
// A typical use of Stay() is to let one ParseHandler detect the start
// of some kind of token, but without moving the read cursor forward.
// When a match is found, it hands off control to another ParseHandler
// to take care of the actual token parsing.
//
// Returns true in case a match was found, false otherwise.
func (a *ParseAPIOnAction) Stay() bool {
if a.ok {
a.parseAPI.result = nil
a.flushTokenAPI()
}
return a.ok
}
func (a *ParseAPIOnAction) flushTokenAPI() {
a.tokenAPI.root.result = newTokenResult()
a.tokenAPI.root.detachChilds()
}
func (a *ParseAPIOnAction) flushReader() {
if a.tokenAPI.offset > 0 {
a.tokenAPI.root.reader.flush(a.tokenAPI.offset)
a.tokenAPI.root.offset = 0
a.parseAPI.initLoopCheck()
}
}
// Result returns a TokenResult struct, containing results as produced by the
// last ParseAPI.On().Accept() call.
func (p *ParseAPI) Result() *TokenResult {
result := p.result
if p.result == nil {
_, filepos := getCaller(1)
panic(fmt.Sprintf(
"parsekit.ParseAPI.TokenResult(): TokenResult() called at %s without "+
"calling ParseAPI.Accept() on beforehand", filepos))
}
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 {
_, filepos := getCaller(2)
panic(fmt.Sprintf("parsekit.ParseAPI.Handle(): Handle() called with nil input at %s", filepos))
}
}
// Expects is used to let a ParseHandler function describe what input it is
// expecting. This expectation is used in error messages to provide some
// context to them.
//
// When defining an expectation inside a ParseHandler, you do not need to
// handle unexpected input yourself. When the end of the parser is reached
// without stopping it using ParseAPI.Stop() or ParseAPI.ExpectEndOfFile(),
// an automatic error will be emitted using ParseAPI.UnexpectedInput().
func (p *ParseAPI) Expects(description string) {
p.panicWhenStoppedOrInError()
p.expecting = description
}
// 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 UnexpectedError().
// 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 = &Error{message, p.tokenAPI.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.UnexpectedInput() with "end of file"
// as the expectation.
func (p *ParseAPI) ExpectEndOfFile() {
p.panicWhenStoppedOrInError()
if p.On(A.EndOfFile).Stay() {
p.Stop()
} else {
p.Expects("end of file")
p.UnexpectedInput()
}
}
// UnexpectedInput is used to set an error that tells the user that some
// unexpected input was encountered.
//
// It can automatically produce an error message for a couple of situations:
// 1) input simply didn't match the expectation
// 2) the end of the input was reached
// 3) there was an invalid UTF8 character on the input.
//
// The parser implementation can provide some feedback for this error by
// calling ParseAPI.Expects() to set the expectation. When set, the
// expectation is included in the error message.
func (p *ParseAPI) UnexpectedInput() {
p.panicWhenStoppedOrInError()
r, err := p.tokenAPI.NextRune()
switch {
case err == nil:
p.Error("unexpected character %q%s", r, fmtExpects(p))
case err == io.EOF:
p.Error("unexpected end of file%s", fmtExpects(p))
default:
p.Error("unexpected error '%s'%s", err, fmtExpects(p))
}
}
func fmtExpects(p *ParseAPI) string {
if p.expecting == "" {
return ""
}
return fmt.Sprintf(" (expected %s)", p.expecting)
}