diff --git a/.gitignore b/.gitignore index 7dcd9340ca..835bdb12a5 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ idpdb.bolt # Project binaries. /idp -/transpilerd +/idpd +/ifqld +/bin diff --git a/Gopkg.lock b/Gopkg.lock index ce1842ddae..52804e457a 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -244,6 +244,16 @@ packages = ["."] revision = "bb74f1db0675b241733089d5a1faa5dd8b0ef57b" +[[projects]] + branch = "master" + name = "github.com/mna/pigeon" + packages = [ + ".", + "ast", + "builder" + ] + revision = "2ad051b8b508f69e7dcf8febdea2856ec2db73ed" + [[projects]] name = "github.com/opentracing/opentracing-go" packages = [ @@ -437,6 +447,16 @@ revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" version = "v0.3.0" +[[projects]] + branch = "master" + name = "golang.org/x/tools" + packages = [ + "go/ast/astutil", + "imports", + "internal/fastwalk" + ] + revision = "28aef64757f4d432485ab970b094e1af8b301e84" + [[projects]] name = "gopkg.in/yaml.v2" packages = ["."] @@ -446,6 +466,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "7e6c508f4349af14fb56dd5feb04d7f1ac10f0c88caf74f12f115c456b039940" + inputs-digest = "ed7f4f2ab4a6f007ba6377ae04b0405146ea60608de96ef76ab19eef8113e694" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 556965ed05..149d89037c 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -25,6 +25,11 @@ # unused-packages = true +required = [ +# Pigeon is the command used to generate the IFQL parser from the PEG description +"github.com/mna/pigeon" +] + [[constraint]] name = "github.com/google/go-cmp" version = "0.2.0" @@ -33,6 +38,13 @@ name = "github.com/influxdata/influxdb" branch = "master" + +# Pigeon hasn't made official releases for a while, we need to use master for now. +# We plan to replace pigeon with a hand written parser, as such this dependency is short lived. +[[override]] + branch = "master" + name = "github.com/mna/pigeon" + # Dependency is pinned to explicit revision rather than latest release because # latest release pre-dates context, which we need. [[constraint]] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..bede5fa42e --- /dev/null +++ b/Makefile @@ -0,0 +1,99 @@ +# Top level Makefile for the entire project +# +# This Makefile follows a few conventions: +# +# * All cmds must be added to this top level Makefile. +# * All binaries are placed in ./bin, its recommended to add this directory to your PATH. +# * Each package that has a need to run go generate, must have its own Makefile for that purpose. +# * All recursive Makefiles must support the targets: all and clean. +# + +SUBDIRS := query + +GO_ARGS=-tags '$(GO_TAGS)' + +# Test vars can be used by all recursive Makefiles +export GO_BUILD=go build $(GO_ARGS) +export GO_TEST=go test $(GO_ARGS) +export GO_GENERATE=go generate $(GO_ARGS) + + +# All go source files +SOURCES := $(shell find . -name '*.go' -not -name '*_test.go') + +# All go source files excluding the vendored sources. +SOURCES_NO_VENDOR := $(shell find . -path ./vendor -prune -o -name "*.go" -not -name '*_test.go' -print) + +# List of binary cmds to build +CMDS := bin/idp bin/idpd bin/ifqld + +# List of utilities to build as part of the build process +UTILS := bin/pigeon bin/cmpgen + +# Default target to build all commands. +# +# This target setups the dependencies to correctly build all commands. +# Other targets must depend on this target to correctly builds CMDS. +all: Gopkg.lock $(UTILS) subdirs $(CMDS) + +# Target to build subdirs. +# Each subdirs must support the `all` target. +subdirs: $(SUBDIRS) + $(MAKE) -C $^ all + +# +# Define targets for commands +# + +bin/ifqld: $(SOURCES) + $(GO_BUILD) -i -o bin/ifqld ./cmd/ifqld + +bin/idp: $(SOURCES) + $(GO_BUILD) -i -o bin/idp ./cmd/idp + +bin/idpd: $(SOURCES) + $(GO_BUILD) -i -o bin/idpd ./cmd/idpd + +# +# Define targets for utilities +# + +bin/pigeon: ./vendor/github.com/mna/pigeon/main.go + go build -i -o bin/pigeon ./vendor/github.com/mna/pigeon + +bin/cmpgen: ./query/ast/asttest/cmpgen/main.go + go build -i -o bin/cmpgen ./query/ast/asttest/cmpgen + +# +# Define how source dependencies are managed +# + +Gopkg.lock: Gopkg.toml + dep ensure -v + +vendor/github.com/mna/pigeon/main.go: Gopkg.lock + dep ensure -v + +# +# Define action only targets +# + +fmt: $(SOURCES_NO_VENDOR) + goimports -w $^ + +test: all + $(GO_TEST) ./... + +test-race: all + $(GO_TEST) -race ./... + +bench: all + $(GO_TEST) -bench=. -run=^$$ ./... + +# Recursively clean all subdirs +clean: $(SUBDIRS) + $(MAKE) -C $^ $(MAKECMDGOALS) + rm -rf bin + +# .PHONY targets represent actions that do not create an actual file. +.PHONY: all subdirs $(SUBDIRS) fmt test test-race bench clean diff --git a/query/Makefile b/query/Makefile index d0a8a697a8..ab752db48b 100644 --- a/query/Makefile +++ b/query/Makefile @@ -1,66 +1,13 @@ -VERSION ?= $(shell git describe --always --tags) -SUBDIRS := ast parser promql -GO_ARGS=-tags '$(GO_TAGS)' -export GO_BUILD=go build $(GO_ARGS) -export GO_TEST=go test $(GO_ARGS) -export GO_GENERATE=go generate $(GO_ARGS) -SOURCES := $(shell find . -name '*.go' -not -name '*_test.go') -SOURCES_NO_VENDOR := $(shell find . -path ./vendor -prune -o -name "*.go" -not -name '*_test.go' -print) +SUBDIRS = ast parser promql -all: Gopkg.lock $(SUBDIRS) bin/platform/query bin/ifqld +subdirs: $(SUBDIRS) -$(SUBDIRS): bin/pigeon bin/cmpgen +$(SUBDIRS): $(MAKE) -C $@ $(MAKECMDGOALS) -bin/platform/query: $(SOURCES) bin/pigeon bin/cmpgen - $(GO_BUILD) -i -o bin/platform/query ./cmd/ifql - -bin/platform/queryd: $(SOURCES) bin/pigeon bin/cmpgen - $(GO_BUILD) -i -o bin/platform/queryd ./cmd/ifqld - -bin/pigeon: ./vendor/github.com/mna/pigeon/main.go - go build -i -o bin/pigeon ./vendor/github.com/mna/pigeon - -bin/cmpgen: ./ast/asttest/cmpgen/main.go - go build -i -o bin/cmpgen ./ast/asttest/cmpgen - -Gopkg.lock: Gopkg.toml - dep ensure -v - -vendor/github.com/mna/pigeon/main.go: Gopkg.lock - dep ensure -v - -fmt: $(SOURCES_NO_VENDOR) - goimports -w $^ - -update: - dep ensure -v -update - -test: Gopkg.lock bin/platform/query - $(GO_TEST) ./... - -test-race: Gopkg.lock bin/platform/query - $(GO_TEST) -race ./... - -bench: Gopkg.lock bin/platform/query - $(GO_TEST) -bench=. -run=^$$ ./... - -bin/goreleaser: - go build -i -o bin/goreleaser ./vendor/github.com/goreleaser/goreleaser - -dist: bin/goreleaser - PATH=./bin:${PATH} goreleaser --rm-dist --release-notes CHANGELOG.md - -release: dist release-docker - -release-docker: - docker build -t quay.io/influxdb/platform/queryd:latest . - docker tag quay.io/influxdb/platform/queryd:latest quay.io/influxdb/ifqld:${VERSION} - docker push quay.io/influxdb/platform/queryd:latest - docker push quay.io/influxdb/platform/queryd:${VERSION} +all: $(SUBDIRS) clean: $(SUBDIRS) - rm -rf bin dist -.PHONY: all clean $(SUBDIRS) update test test-race bench release docker dist fmt +.PHONY: all clean subdirs $(SUBDIRS) diff --git a/query/ast/Makefile b/query/ast/Makefile index 4ca2b9c34f..e9876b5fd3 100644 --- a/query/ast/Makefile +++ b/query/ast/Makefile @@ -1,9 +1,13 @@ -SUBDIRS := asttest +SUBDIRS = asttest + +subdirs: $(SUBDIRS) $(SUBDIRS): $(MAKE) -C $@ $(MAKECMDGOALS) all: $(SUBDIRS) +clean: $(SUBDIRS) -.PHONY: $(SUBDIRS) clean + +.PHONY: all clean subdirs $(SUBDIRS) diff --git a/query/ast/asttest/Makefile b/query/ast/asttest/Makefile index a1dced1568..366e304e82 100644 --- a/query/ast/asttest/Makefile +++ b/query/ast/asttest/Makefile @@ -1,7 +1,7 @@ all: cmpopts.go -cmpopts.go: ../ast.go gen.go ../../bin/cmpgen - PATH=../../bin:${PATH} $(GO_GENERATE) -x ./... +cmpopts.go: ../ast.go gen.go ../../../bin/cmpgen + PATH=../../../bin:${PATH} $(GO_GENERATE) -x ./... clean: rm -f cmpopts.go diff --git a/query/dependency.go b/query/dependency.go index 11eda7ddaf..376958a62f 100644 --- a/query/dependency.go +++ b/query/dependency.go @@ -3,8 +3,8 @@ package query import ( "context" - "github.com/influxdata/platform/query/id" "github.com/influxdata/platform" + "github.com/influxdata/platform/query/id" ) // FromBucketService wraps an platform.BucketService in the BucketLookup interface. diff --git a/query/functions/from.go b/query/functions/from.go index f24370572b..2c1a771cba 100644 --- a/query/functions/from.go +++ b/query/functions/from.go @@ -3,11 +3,11 @@ package functions import ( "fmt" + "github.com/influxdata/platform/query" + "github.com/influxdata/platform/query/execute" "github.com/influxdata/platform/query/functions/storage" "github.com/influxdata/platform/query/id" "github.com/influxdata/platform/query/interpreter" - "github.com/influxdata/platform/query" - "github.com/influxdata/platform/query/execute" "github.com/influxdata/platform/query/plan" "github.com/influxdata/platform/query/semantic" "github.com/pkg/errors" diff --git a/query/influxql/transpiler.go b/query/influxql/transpiler.go index b01c79c7ec..e5418cd494 100644 --- a/query/influxql/transpiler.go +++ b/query/influxql/transpiler.go @@ -6,12 +6,12 @@ import ( "fmt" "time" - "github.com/influxdata/platform/query/ast" - "github.com/influxdata/platform/query/functions" - "github.com/influxdata/platform/query" - "github.com/influxdata/platform/query/execute" - "github.com/influxdata/platform/query/semantic" "github.com/influxdata/influxql" + "github.com/influxdata/platform/query" + "github.com/influxdata/platform/query/ast" + "github.com/influxdata/platform/query/execute" + "github.com/influxdata/platform/query/functions" + "github.com/influxdata/platform/query/semantic" ) // Transpiler converts InfluxQL queries into a query spec. diff --git a/query/parser/Makefile b/query/parser/Makefile index 36f7bb2936..cb1fc969f7 100644 --- a/query/parser/Makefile +++ b/query/parser/Makefile @@ -1,8 +1,7 @@ all: ifql.go - -ifql.go: ifql.peg parser.go parser_debug.go ../bin/pigeon - PATH=../bin:${PATH} $(GO_GENERATE) -x ./... +ifql.go: ifql.peg parser.go parser_debug.go ../../bin/pigeon + PATH=../../bin:${PATH} $(GO_GENERATE) -x ./... clean: rm -f ifql.go diff --git a/query/parser/ifql.go b/query/parser/ifql.go index cd67034d36..d7cd4318e0 100644 --- a/query/parser/ifql.go +++ b/query/parser/ifql.go @@ -6961,6 +6961,10 @@ var ( // errNoRule is returned when the grammar to parse has no rule. errNoRule = errors.New("grammar has no rule") + // errInvalidEntrypoint is returned when the specified entrypoint rule + // does not exit. + errInvalidEntrypoint = errors.New("invalid entrypoint") + // errInvalidEncoding is returned when the source is not properly // utf8-encoded. errInvalidEncoding = errors.New("invalid encoding") @@ -6987,6 +6991,38 @@ func MaxExpressions(maxExprCnt uint64) Option { } } +// Entrypoint creates an Option to set the rule name to use as entrypoint. +// The rule name must have been specified in the -alternate-entrypoints +// if generating the parser with the -optimize-grammar flag, otherwise +// it may have been optimized out. Passing an empty string sets the +// entrypoint to the first rule in the grammar. +// +// The default is to start parsing at the first rule in the grammar. +func Entrypoint(ruleName string) Option { + return func(p *parser) Option { + oldEntrypoint := p.entrypoint + p.entrypoint = ruleName + if ruleName == "" { + p.entrypoint = g.rules[0].name + } + return Entrypoint(oldEntrypoint) + } +} + +// AllowInvalidUTF8 creates an Option to allow invalid UTF-8 bytes. +// Every invalid UTF-8 byte is treated as a utf8.RuneError (U+FFFD) +// by character class matchers and is matched by the any matcher. +// The returned matched value, c.text and c.offset are NOT affected. +// +// The default is false. +func AllowInvalidUTF8(b bool) Option { + return func(p *parser) Option { + old := p.allowInvalidUTF8 + p.allowInvalidUTF8 = b + return AllowInvalidUTF8(old) + } +} + // Recover creates an Option to set the recover flag to b. When set to // true, this causes the parser to recover from panics and convert it // to an error. Setting it to false can be useful while debugging to @@ -7063,10 +7099,16 @@ type current struct { pos position // start position of the match text []byte // raw text of the match - // the globalStore allows the parser to store arbitrary values - globalStore map[string]interface{} + // globalStore is a general store for the user to store arbitrary key-value + // pairs that they need to manage and that they do not want tied to the + // backtracking of the parser. This is only modified by the user and never + // rolled back by the parser. It is always up to the user to keep this in a + // consistent state. + globalStore storeDict } +type storeDict map[string]interface{} + // the AST types... type grammar struct { @@ -7233,11 +7275,13 @@ func newParser(filename string, b []byte, opts ...Option) *parser { pt: savepoint{position: position{line: 1}}, recover: true, cur: current{ - globalStore: make(map[string]interface{}), + globalStore: make(storeDict), }, maxFailPos: position{col: 1, line: 1}, maxFailExpected: make([]string, 0, 20), Stats: &stats, + // start rule is rule [0] unless an alternate entrypoint is specified + entrypoint: g.rules[0].name, } p.setOptions(opts) @@ -7310,12 +7354,19 @@ type parser struct { // max number of expressions to be parsed maxExprCnt uint64 + // entrypoint for the parser + entrypoint string + + allowInvalidUTF8 bool *Stats choiceNoMatch string // recovery expression stack, keeps track of the currently available recovery expression, these are traversed in reverse recoveryStack []map[string]interface{} + + // emptyState contains an empty storeDict, which is used to optimize cloneState if global "state" store is not used. + emptyState storeDict } // push a variable set on the vstack. @@ -7434,8 +7485,8 @@ func (p *parser) read() { p.pt.col = 0 } - if rn == utf8.RuneError { - if n == 1 { + if rn == utf8.RuneError && n == 1 { // see utf8.DecodeRune + if !p.allowInvalidUTF8 { p.addErr(errInvalidEncoding) } } @@ -7487,9 +7538,14 @@ func (p *parser) parse(g *grammar) (val interface{}, err error) { }() } - // start rule is rule [0] + startRule, ok := p.rules[p.entrypoint] + if !ok { + p.addErr(errInvalidEntrypoint) + return nil, p.errs.err() + } + p.read() // advance to first rune - val, ok := p.parseRule(g.rules[0]) + val, ok = p.parseRule(startRule) if !ok { if len(*p.errs) == 0 { // If parsing fails, but no errors have been recorded, the expected values @@ -7599,16 +7655,19 @@ func (p *parser) parseActionExpr(act *actionExpr) (interface{}, bool) { if err != nil { p.addErrAt(err, start.position, []string{}) } + val = actVal } return val, ok } func (p *parser) parseAndCodeExpr(and *andCodeExpr) (interface{}, bool) { + ok, err := and.run(p) if err != nil { p.addErr(err) } + return nil, ok } @@ -7618,18 +7677,20 @@ func (p *parser) parseAndExpr(and *andExpr) (interface{}, bool) { _, ok := p.parseExpr(and.expr) p.popV() p.restore(pt) + return nil, ok } func (p *parser) parseAnyMatcher(any *anyMatcher) (interface{}, bool) { - if p.pt.rn != utf8.RuneError { - start := p.pt - p.read() - p.failAt(true, start.position, ".") - return p.sliceFrom(start), true + if p.pt.rn == utf8.RuneError && p.pt.w == 0 { + // EOF - see utf8.DecodeRune + p.failAt(false, p.pt.position, ".") + return nil, false } - p.failAt(false, p.pt.position, ".") - return nil, false + start := p.pt + p.read() + p.failAt(true, start.position, ".") + return p.sliceFrom(start), true } func (p *parser) parseCharClassMatcher(chr *charClassMatcher) (interface{}, bool) { @@ -7637,7 +7698,7 @@ func (p *parser) parseCharClassMatcher(chr *charClassMatcher) (interface{}, bool start := p.pt // can't match EOF - if cur == utf8.RuneError { + if cur == utf8.RuneError && p.pt.w == 0 { // see utf8.DecodeRune p.failAt(false, start.position, chr.val) return nil, false } @@ -7748,6 +7809,7 @@ func (p *parser) parseNotCodeExpr(not *notCodeExpr) (interface{}, bool) { if err != nil { p.addErr(err) } + return nil, !ok } @@ -7759,6 +7821,7 @@ func (p *parser) parseNotExpr(not *notExpr) (interface{}, bool) { p.maxFailInvertExpected = !p.maxFailInvertExpected p.popV() p.restore(pt) + return nil, !ok } diff --git a/query/promql/Makefile b/query/promql/Makefile index db33da0fb3..06e102e6a4 100644 --- a/query/promql/Makefile +++ b/query/promql/Makefile @@ -1,7 +1,7 @@ all: promql.go -promql.go: promql.peg gen.go ../bin/pigeon - PATH=../bin:${PATH} go generate -x ./... +promql.go: promql.peg gen.go ../../bin/pigeon + PATH=../../bin:${PATH} go generate -x ./... clean: rm -f promql.go diff --git a/query/promql/promql.go b/query/promql/promql.go index 5bdfcba9ad..f775e173fb 100644 --- a/query/promql/promql.go +++ b/query/promql/promql.go @@ -3618,6 +3618,10 @@ var ( // errNoRule is returned when the grammar to parse has no rule. errNoRule = errors.New("grammar has no rule") + // errInvalidEntrypoint is returned when the specified entrypoint rule + // does not exit. + errInvalidEntrypoint = errors.New("invalid entrypoint") + // errInvalidEncoding is returned when the source is not properly // utf8-encoded. errInvalidEncoding = errors.New("invalid encoding") @@ -3644,6 +3648,24 @@ func MaxExpressions(maxExprCnt uint64) Option { } } +// Entrypoint creates an Option to set the rule name to use as entrypoint. +// The rule name must have been specified in the -alternate-entrypoints +// if generating the parser with the -optimize-grammar flag, otherwise +// it may have been optimized out. Passing an empty string sets the +// entrypoint to the first rule in the grammar. +// +// The default is to start parsing at the first rule in the grammar. +func Entrypoint(ruleName string) Option { + return func(p *parser) Option { + oldEntrypoint := p.entrypoint + p.entrypoint = ruleName + if ruleName == "" { + p.entrypoint = g.rules[0].name + } + return Entrypoint(oldEntrypoint) + } +} + // Statistics adds a user provided Stats struct to the parser to allow // the user to process the results after the parsing has finished. // Also the key for the "no match" counter is set. @@ -3701,6 +3723,20 @@ func Memoize(b bool) Option { } } +// AllowInvalidUTF8 creates an Option to allow invalid UTF-8 bytes. +// Every invalid UTF-8 byte is treated as a utf8.RuneError (U+FFFD) +// by character class matchers and is matched by the any matcher. +// The returned matched value, c.text and c.offset are NOT affected. +// +// The default is false. +func AllowInvalidUTF8(b bool) Option { + return func(p *parser) Option { + old := p.allowInvalidUTF8 + p.allowInvalidUTF8 = b + return AllowInvalidUTF8(old) + } +} + // Recover creates an Option to set the recover flag to b. When set to // true, this causes the parser to recover from panics and convert it // to an error. Setting it to false can be useful while debugging to @@ -3725,6 +3761,16 @@ func GlobalStore(key string, value interface{}) Option { } } +// InitState creates an Option to set a key to a certain value in +// the global "state" store. +func InitState(key string, value interface{}) Option { + return func(p *parser) Option { + old := p.cur.state[key] + p.cur.state[key] = value + return InitState(key, old) + } +} + // ParseFile parses the file identified by filename. func ParseFile(filename string, opts ...Option) (i interface{}, err error) { f, err := os.Open(filename) @@ -3777,10 +3823,21 @@ type current struct { pos position // start position of the match text []byte // raw text of the match - // the globalStore allows the parser to store arbitrary values - globalStore map[string]interface{} + // state is a store for arbitrary key,value pairs that the user wants to be + // tied to the backtracking of the parser. + // This is always rolled back if a parsing rule fails. + state storeDict + + // globalStore is a general store for the user to store arbitrary key-value + // pairs that they need to manage and that they do not want tied to the + // backtracking of the parser. This is only modified by the user and never + // rolled back by the parser. It is always up to the user to keep this in a + // consistent state. + globalStore storeDict } +type storeDict map[string]interface{} + // the AST types... type grammar struct { @@ -3845,6 +3902,11 @@ type ruleRefExpr struct { name string } +type stateCodeExpr struct { + pos position + run func(*parser) error +} + type andCodeExpr struct { pos position run func(*parser) (bool, error) @@ -3947,11 +4009,15 @@ func newParser(filename string, b []byte, opts ...Option) *parser { pt: savepoint{position: position{line: 1}}, recover: true, cur: current{ - globalStore: make(map[string]interface{}), + state: make(storeDict), + globalStore: make(storeDict), }, maxFailPos: position{col: 1, line: 1}, maxFailExpected: make([]string, 0, 20), Stats: &stats, + // start rule is rule [0] unless an alternate entrypoint is specified + entrypoint: g.rules[0].name, + emptyState: make(storeDict), } p.setOptions(opts) @@ -4030,12 +4096,19 @@ type parser struct { // max number of expressions to be parsed maxExprCnt uint64 + // entrypoint for the parser + entrypoint string + + allowInvalidUTF8 bool *Stats choiceNoMatch string // recovery expression stack, keeps track of the currently available recovery expression, these are traversed in reverse recoveryStack []map[string]interface{} + + // emptyState contains an empty storeDict, which is used to optimize cloneState if global "state" store is not used. + emptyState storeDict } // push a variable set on the vstack. @@ -4174,8 +4247,8 @@ func (p *parser) read() { p.pt.col = 0 } - if rn == utf8.RuneError { - if n == 1 { + if rn == utf8.RuneError && n == 1 { // see utf8.DecodeRune + if !p.allowInvalidUTF8 { p.addErr(errInvalidEncoding) } } @@ -4192,6 +4265,50 @@ func (p *parser) restore(pt savepoint) { p.pt = pt } +// Cloner is implemented by any value that has a Clone method, which returns a +// copy of the value. This is mainly used for types which are not passed by +// value (e.g map, slice, chan) or structs that contain such types. +// +// This is used in conjunction with the global state feature to create proper +// copies of the state to allow the parser to properly restore the state in +// the case of backtracking. +type Cloner interface { + Clone() interface{} +} + +// clone and return parser current state. +func (p *parser) cloneState() storeDict { + if p.debug { + defer p.out(p.in("cloneState")) + } + + if len(p.cur.state) == 0 { + if len(p.emptyState) > 0 { + p.emptyState = make(storeDict) + } + return p.emptyState + } + + state := make(storeDict, len(p.cur.state)) + for k, v := range p.cur.state { + if c, ok := v.(Cloner); ok { + state[k] = c.Clone() + } else { + state[k] = v + } + } + return state +} + +// restore parser current state to the state storeDict. +// every restoreState should applied only one time for every cloned state +func (p *parser) restoreState(state storeDict) { + if p.debug { + defer p.out(p.in("restoreState")) + } + p.cur.state = state +} + // get the slice of bytes from the savepoint start to the current position. func (p *parser) sliceFrom(start savepoint) []byte { return p.data[start.position.offset:p.pt.position.offset] @@ -4257,9 +4374,14 @@ func (p *parser) parse(g *grammar) (val interface{}, err error) { }() } - // start rule is rule [0] + startRule, ok := p.rules[p.entrypoint] + if !ok { + p.addErr(errInvalidEntrypoint) + return nil, p.errs.err() + } + p.read() // advance to first rune - val, ok := p.parseRule(g.rules[0]) + val, ok = p.parseRule(startRule) if !ok { if len(*p.errs) == 0 { // If parsing fails, but no errors have been recorded, the expected values @@ -4377,6 +4499,8 @@ func (p *parser) parseExpr(expr interface{}) (interface{}, bool) { val, ok = p.parseRuleRefExpr(expr) case *seqExpr: val, ok = p.parseSeqExpr(expr) + case *stateCodeExpr: + val, ok = p.parseStateCodeExpr(expr) case *throwExpr: val, ok = p.parseThrowExpr(expr) case *zeroOrMoreExpr: @@ -4402,10 +4526,13 @@ func (p *parser) parseActionExpr(act *actionExpr) (interface{}, bool) { if ok { p.cur.pos = start.position p.cur.text = p.sliceFrom(start) + state := p.cloneState() actVal, err := act.run(p) if err != nil { p.addErrAt(err, start.position, []string{}) } + p.restoreState(state) + val = actVal } if ok && p.debug { @@ -4419,10 +4546,14 @@ func (p *parser) parseAndCodeExpr(and *andCodeExpr) (interface{}, bool) { defer p.out(p.in("parseAndCodeExpr")) } + state := p.cloneState() + ok, err := and.run(p) if err != nil { p.addErr(err) } + p.restoreState(state) + return nil, ok } @@ -4432,10 +4563,13 @@ func (p *parser) parseAndExpr(and *andExpr) (interface{}, bool) { } pt := p.pt + state := p.cloneState() p.pushV() _, ok := p.parseExpr(and.expr) p.popV() + p.restoreState(state) p.restore(pt) + return nil, ok } @@ -4444,14 +4578,15 @@ func (p *parser) parseAnyMatcher(any *anyMatcher) (interface{}, bool) { defer p.out(p.in("parseAnyMatcher")) } - if p.pt.rn != utf8.RuneError { - start := p.pt - p.read() - p.failAt(true, start.position, ".") - return p.sliceFrom(start), true + if p.pt.rn == utf8.RuneError && p.pt.w == 0 { + // EOF - see utf8.DecodeRune + p.failAt(false, p.pt.position, ".") + return nil, false } - p.failAt(false, p.pt.position, ".") - return nil, false + start := p.pt + p.read() + p.failAt(true, start.position, ".") + return p.sliceFrom(start), true } func (p *parser) parseCharClassMatcher(chr *charClassMatcher) (interface{}, bool) { @@ -4463,7 +4598,7 @@ func (p *parser) parseCharClassMatcher(chr *charClassMatcher) (interface{}, bool start := p.pt // can't match EOF - if cur == utf8.RuneError { + if cur == utf8.RuneError && p.pt.w == 0 { // see utf8.DecodeRune p.failAt(false, start.position, chr.val) return nil, false } @@ -4544,6 +4679,8 @@ func (p *parser) parseChoiceExpr(ch *choiceExpr) (interface{}, bool) { // dummy assignment to prevent compile error if optimized _ = altI + state := p.cloneState() + p.pushV() val, ok := p.parseExpr(alt) p.popV() @@ -4551,6 +4688,7 @@ func (p *parser) parseChoiceExpr(ch *choiceExpr) (interface{}, bool) { p.incChoiceAltCnt(ch, altI) return val, ok } + p.restoreState(state) } p.incChoiceAltCnt(ch, choiceNoMatch) return nil, false @@ -4603,10 +4741,14 @@ func (p *parser) parseNotCodeExpr(not *notCodeExpr) (interface{}, bool) { defer p.out(p.in("parseNotCodeExpr")) } + state := p.cloneState() + ok, err := not.run(p) if err != nil { p.addErr(err) } + p.restoreState(state) + return nil, !ok } @@ -4616,12 +4758,15 @@ func (p *parser) parseNotExpr(not *notExpr) (interface{}, bool) { } pt := p.pt + state := p.cloneState() p.pushV() p.maxFailInvertExpected = !p.maxFailInvertExpected _, ok := p.parseExpr(not.expr) p.maxFailInvertExpected = !p.maxFailInvertExpected p.popV() + p.restoreState(state) p.restore(pt) + return nil, !ok } @@ -4684,9 +4829,11 @@ func (p *parser) parseSeqExpr(seq *seqExpr) (interface{}, bool) { vals := make([]interface{}, 0, len(seq.exprs)) pt := p.pt + state := p.cloneState() for _, expr := range seq.exprs { val, ok := p.parseExpr(expr) if !ok { + p.restoreState(state) p.restore(pt) return nil, false } @@ -4695,6 +4842,18 @@ func (p *parser) parseSeqExpr(seq *seqExpr) (interface{}, bool) { return vals, true } +func (p *parser) parseStateCodeExpr(state *stateCodeExpr) (interface{}, bool) { + if p.debug { + defer p.out(p.in("parseStateCodeExpr")) + } + + err := state.run(p) + if err != nil { + p.addErr(err) + } + return nil, true +} + func (p *parser) parseThrowExpr(expr *throwExpr) (interface{}, bool) { if p.debug { defer p.out(p.in("parseThrowExpr")) diff --git a/query/promql/types.go b/query/promql/types.go index f6d62bc4d2..042749c0b1 100644 --- a/query/promql/types.go +++ b/query/promql/types.go @@ -6,9 +6,9 @@ import ( "strings" "time" + "github.com/influxdata/platform/query" "github.com/influxdata/platform/query/ast" "github.com/influxdata/platform/query/functions" - "github.com/influxdata/platform/query" "github.com/influxdata/platform/query/semantic" ) diff --git a/query/query_test/normalize_text/normalize.go b/query/query_test/normalize_text/normalize.go index 851a39f9e6..5958317376 100644 --- a/query/query_test/normalize_text/normalize.go +++ b/query/query_test/normalize_text/normalize.go @@ -2,11 +2,12 @@ package main import ( "fmt" - "golang.org/x/text/unicode/norm" "io/ioutil" "os" "regexp" "strings" + + "golang.org/x/text/unicode/norm" ) func normalizeString(s string) []byte {