diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8f3e5c5 --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +test: + @(cd read; go test | grep -v ^PASS) + @(cd tokenize; go test | grep -v ^PASS) + @(cd parse; go test | grep -v ^PASS) + @(cd examples; go test | grep -v ^PASS) diff --git a/parse/api.go b/parse/api.go index e92712d..8a64599 100644 --- a/parse/api.go +++ b/parse/api.go @@ -16,11 +16,24 @@ import ( // // • 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 // last tokenize.Handler result as produced by Accept() or Peek() - loopCheck map[string]bool // used for parser loop detection - err error // parse error, retrieved by Error(), using API methods is denied when set - stopped bool // a boolean set to true by Stop(), using API methods is denied when true + tokenAPI *tokenize.API // the tokenize.API, used for communicating with tokenize.Handler functions + result *tokenize.Result // last tokenize.Handler result as produced by Accept() or Peek() + sanityChecksEnabled bool // whether or not runtime sanity checks are enabled + loopCheck map[filepos]bool // used for parser loop detection + err error // parse error, retrieved by Error(), using API methods is denied when set + stopped bool // a boolean set to true by Stop() +} + +// DisableSanityChecks disables the built-in parser implementation sanity checks, +// which detects parser implementation errors like loops and continuing parsing +// after an error or invoking Stop(). +// +// These tests do cause a performance hit. When your parser has to handle a lot +// of input data and is fairly complex, you might want to disable the sanity +// checks. When you're not sure, You probably don't want to use this method, +// and enjoy the added safety of the built-in checks. +func (p *API) DisableSanityChecks() { + p.sanityChecksEnabled = true } // Peek checks if the upcoming input data matches the provided tokenize.Handler. @@ -54,7 +67,7 @@ func (p *API) Accept(tokenHandler tokenize.Handler) bool { forkedAPI.Merge() p.result = p.tokenAPI.Result() forkedAPI.Dispose() - if p.tokenAPI.FlushInput() { + if p.sanityChecksEnabled && p.tokenAPI.FlushInput() { p.initLoopCheck() } } @@ -62,10 +75,12 @@ func (p *API) Accept(tokenHandler tokenize.Handler) bool { } func (p *API) invokeHandler(name string, tokenHandler tokenize.Handler) (*tokenize.API, bool) { - p.panicWhenStoppedOrInError(name) - p.checkForLoops(name) - if tokenHandler == nil { - callerPanic(name, "parsekit.parse.API.{name}(): {name}() called with nil tokenHandler argument at {caller}") + if p.sanityChecksEnabled { + p.panicWhenStoppedOrInError(name) + p.checkForLoops(name) + if tokenHandler == nil { + callerPanic(name, "parsekit.parse.API.{name}(): {name}() called with nil tokenHandler argument at {caller}") + } } p.result = nil @@ -108,7 +123,7 @@ func (p *API) IsStoppedOrInError() bool { // When Accept() is called, and the parser moved forward in the input data, // this method is called to reset the map for the new read cursor position. func (p *API) initLoopCheck() { - p.loopCheck = map[string]bool{} + p.loopCheck = make(map[filepos]bool) } // checkForLoops checks if the line of code from which Accept() or Peek() @@ -152,9 +167,13 @@ func (p *API) Result() *tokenize.Result { // 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 (p *API) Handle(parseHandler ...Handler) bool { - p.panicWhenStoppedOrInError("Handle") + if p.sanityChecksEnabled { + p.panicWhenStoppedOrInError("Handle") + } for _, handler := range parseHandler { - p.panicWhenHandlerNil("Handle", handler) + if p.sanityChecksEnabled { + p.panicWhenHandlerNil("Handle", handler) + } handler(p) if p.IsStoppedOrInError() { return false @@ -204,7 +223,9 @@ func (p *API) Error(format string, data ...interface{}) { // will be stopped through Stop(). Otherwise, the unexpected input is reported // using Expected("end of file"). func (p *API) ExpectEndOfFile() { - p.panicWhenStoppedOrInError("ExpectEndofFile") + if p.sanityChecksEnabled { + p.panicWhenStoppedOrInError("ExpectEndofFile") + } if p.Peek(tokenize.A.EndOfFile) { p.Stop() } else { @@ -226,7 +247,9 @@ func (p *API) ExpectEndOfFile() { // // • there was an error while reading the input. func (p *API) Expected(expected string) { - p.panicWhenStoppedOrInError("Expected") + if p.sanityChecksEnabled { + p.panicWhenStoppedOrInError("Expected") + } _, err := p.tokenAPI.NextRune() switch { case err == nil: diff --git a/parse/callerinfo.go b/parse/callerinfo.go index cb877ba..d5e9664 100644 --- a/parse/callerinfo.go +++ b/parse/callerinfo.go @@ -24,10 +24,19 @@ func callerBefore(name string) string { } } -func callerFilepos(depth int) string { +type filepos struct { + file string + line int +} + +func (pos *filepos) String() string { + return fmt.Sprintf("%s:%d", pos.file, pos.line) +} + +func callerFilepos(depth int) filepos { // No error handling, because we call this method ourselves with safe depth values. _, file, line, _ := runtime.Caller(depth + 1) - return fmt.Sprintf("%s:%d", file, line) + return filepos{file, line} } func callerPanic(name, f string, data ...interface{}) { diff --git a/parse/parse.go b/parse/parse.go index 3e157e6..d2378e2 100644 --- a/parse/parse.go +++ b/parse/parse.go @@ -26,13 +26,31 @@ type Func func(interface{}) error // against the provided input data. For an overview of allowed inputs, take a // look at the documentation for parsekit.read.New(). func New(startHandler Handler) Func { + return new(startHandler, true) +} + +// NewWithoutSanityChecks instantiates a new parser, which does not have +// parsekit's built-in sanith checks enabled (e.g. checks for loops or +// or calls to parse.API methods after an error or Stop()). +// +// Disabling sanity checks does improve parsing performance, but for +// most use cases this is not an issue. Only disable sanity checks when +// you really need the extra performance. +// You can of course create a normal sanity-checked parser that is used +// during development / unit testing, and an unchecked one for production. +func NewWithoutSanityChecks(startHandler Handler) Func { + return new(startHandler, false) +} + +func new(startHandler Handler, sanityChecksEnabled bool) Func { if startHandler == nil { callerPanic("New", "parsekit.parse.{name}(): {name}() called with nil input at {caller}") } return func(input interface{}) error { api := &API{ - tokenAPI: tokenize.NewAPI(input), - loopCheck: map[string]bool{}, + tokenAPI: tokenize.NewAPI(input), + loopCheck: make(map[filepos]bool), + sanityChecksEnabled: sanityChecksEnabled, } if api.Handle(startHandler) { // Handle returned true, indicating that parsing could still continue.