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) }