689 lines
17 KiB
Go
689 lines
17 KiB
Go
// Copyright 2019 PingCAP, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package parser
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
hash2 "hash"
|
|
"strings"
|
|
"sync"
|
|
"unsafe"
|
|
|
|
"github.com/pingcap/errors"
|
|
"github.com/pingcap/tidb/pkg/parser/charset"
|
|
)
|
|
|
|
// Digest stores the fixed length hash value.
|
|
type Digest struct {
|
|
b []byte
|
|
str string
|
|
}
|
|
|
|
// NewDigest returns a new digest.
|
|
func NewDigest(b []byte) *Digest {
|
|
return &Digest{
|
|
b: b,
|
|
str: hex.EncodeToString(b),
|
|
}
|
|
}
|
|
|
|
// String returns the digest hex string.
|
|
func (d *Digest) String() string {
|
|
return d.str
|
|
}
|
|
|
|
// Bytes returns the digest byte slice.
|
|
func (d *Digest) Bytes() []byte {
|
|
return d.b
|
|
}
|
|
|
|
// DigestHash generates the digest of statements.
|
|
// it will generate a hash on normalized form of statement text
|
|
// which removes general property of a statement but keeps specific property.
|
|
//
|
|
// for example: both DigestHash('select 1') and DigestHash('select 2') => e1c71d1661ae46e09b7aaec1c390957f0d6260410df4e4bc71b9c8d681021471
|
|
//
|
|
// Deprecated: It is logically consistent with NormalizeDigest.
|
|
func DigestHash(sql string) (digest *Digest) {
|
|
d := digesterPool.Get().(*sqlDigester)
|
|
digest = d.doDigest(sql)
|
|
digesterPool.Put(d)
|
|
return
|
|
}
|
|
|
|
// DigestNormalized generates the digest of a normalized sql.
|
|
// it will generate a hash on a normalized sql.
|
|
// Normalize + DigestNormalized equals to NormalizeDigest.
|
|
//
|
|
// for example: DigestNormalized('select ?')
|
|
// DigestNormalized should be called with a normalized SQL string (like 'select ?') generated by function Normalize.
|
|
// do not call with SQL which is not normalized, DigestNormalized('select 1') and DigestNormalized('select 2') is not the same
|
|
func DigestNormalized(normalized string) (digest *Digest) {
|
|
d := digesterPool.Get().(*sqlDigester)
|
|
digest = d.doDigestNormalized(normalized)
|
|
digesterPool.Put(d)
|
|
return
|
|
}
|
|
|
|
// Normalize generates the normalized statements.
|
|
// it will get normalized form of statement text
|
|
// which removes general property of a statement but keeps specific property.
|
|
// possible values for 'redact' is "OFF", "ON" or "MARKER". Passing "" is seen as "OFF".
|
|
//
|
|
// when "OFF", it is returned as is
|
|
// for example, when "ON": Normalize('select 1 from b where a = 1') => 'select ? from b where a = ?'
|
|
// for example, when "MARKER": Normalize('select 1 from b where a = 1') => 'select ‹1› from b where a = ‹1›'
|
|
func Normalize(sql string, redact string) (result string) {
|
|
if redact == "" || redact == errors.RedactLogDisable {
|
|
return sql
|
|
}
|
|
d := digesterPool.Get().(*sqlDigester)
|
|
result = d.doNormalize(sql, redact, false)
|
|
digesterPool.Put(d)
|
|
return
|
|
}
|
|
|
|
// NormalizeForBinding generates the normalized statements with additional binding rules
|
|
// it will get normalized form of statement text
|
|
// which removes general property of a statement but keeps specific property.
|
|
//
|
|
// for example: NormalizeForBinding('select 1 from b where a = 1') => 'select ? from b where a = ?'
|
|
func NormalizeForBinding(sql string, forPlanReplayerReload bool) (result string) {
|
|
d := digesterPool.Get().(*sqlDigester)
|
|
result = d.doNormalizeForBinding(sql, false, forPlanReplayerReload)
|
|
digesterPool.Put(d)
|
|
return
|
|
}
|
|
|
|
// NormalizeKeepHint generates the normalized statements, but keep the hints.
|
|
// it will get normalized form of statement text with hints.
|
|
// which removes general property of a statement but keeps specific property.
|
|
//
|
|
// for example: Normalize('select /*+ use_index(t, primary) */ 1 from b where a = 1') => 'select /*+ use_index(t, primary) */ ? from b where a = ?'
|
|
func NormalizeKeepHint(sql string) (result string) {
|
|
d := digesterPool.Get().(*sqlDigester)
|
|
result = d.doNormalize(sql, errors.RedactLogEnable, true)
|
|
digesterPool.Put(d)
|
|
return
|
|
}
|
|
|
|
// NormalizeDigest combines Normalize and DigestNormalized into one method.
|
|
func NormalizeDigest(sql string) (normalized string, digest *Digest) {
|
|
d := digesterPool.Get().(*sqlDigester)
|
|
normalized, digest = d.doNormalizeDigest(sql)
|
|
digesterPool.Put(d)
|
|
return
|
|
}
|
|
|
|
// NormalizeDigestForBinding combines Normalize and DigestNormalized into one method with additional binding rules.
|
|
func NormalizeDigestForBinding(sql string) (normalized string, digest *Digest) {
|
|
d := digesterPool.Get().(*sqlDigester)
|
|
normalized, digest = d.doNormalizeDigestForBinding(sql)
|
|
digesterPool.Put(d)
|
|
return
|
|
}
|
|
|
|
var digesterPool = sync.Pool{
|
|
New: func() interface{} {
|
|
return &sqlDigester{
|
|
lexer: NewScanner(""),
|
|
hasher: sha256.New(),
|
|
}
|
|
},
|
|
}
|
|
|
|
// sqlDigester is used to compute DigestHash or Normalize for sql.
|
|
type sqlDigester struct {
|
|
buffer bytes.Buffer
|
|
lexer *Scanner
|
|
hasher hash2.Hash
|
|
tokens tokenDeque
|
|
}
|
|
|
|
func (d *sqlDigester) doDigestNormalized(normalized string) (digest *Digest) {
|
|
b := unsafe.Slice(unsafe.StringData(normalized), len(normalized))
|
|
d.hasher.Write(b)
|
|
digest = NewDigest(d.hasher.Sum(nil))
|
|
d.hasher.Reset()
|
|
return
|
|
}
|
|
|
|
func (d *sqlDigester) doDigest(sql string) (digest *Digest) {
|
|
d.normalize(sql, errors.RedactLogEnable, false, false, false)
|
|
d.hasher.Write(d.buffer.Bytes())
|
|
d.buffer.Reset()
|
|
digest = NewDigest(d.hasher.Sum(nil))
|
|
d.hasher.Reset()
|
|
return
|
|
}
|
|
|
|
func (d *sqlDigester) doNormalize(sql string, redact string, keepHint bool) (result string) {
|
|
d.normalize(sql, redact, keepHint, false, false)
|
|
result = d.buffer.String()
|
|
d.buffer.Reset()
|
|
return
|
|
}
|
|
|
|
func (d *sqlDigester) doNormalizeForBinding(sql string, keepHint bool, forPlanReplayerReload bool) (result string) {
|
|
d.normalize(sql, errors.RedactLogEnable, keepHint, true, forPlanReplayerReload)
|
|
result = d.buffer.String()
|
|
d.buffer.Reset()
|
|
return
|
|
}
|
|
|
|
func (d *sqlDigester) doNormalizeDigest(sql string) (normalized string, digest *Digest) {
|
|
d.normalize(sql, errors.RedactLogEnable, false, false, false)
|
|
normalized = d.buffer.String()
|
|
d.hasher.Write(d.buffer.Bytes())
|
|
d.buffer.Reset()
|
|
digest = NewDigest(d.hasher.Sum(nil))
|
|
d.hasher.Reset()
|
|
return
|
|
}
|
|
|
|
func (d *sqlDigester) doNormalizeDigestForBinding(sql string) (normalized string, digest *Digest) {
|
|
d.normalize(sql, errors.RedactLogEnable, false, true, false)
|
|
normalized = d.buffer.String()
|
|
d.hasher.Write(d.buffer.Bytes())
|
|
d.buffer.Reset()
|
|
digest = NewDigest(d.hasher.Sum(nil))
|
|
d.hasher.Reset()
|
|
return
|
|
}
|
|
|
|
const (
|
|
// genericSymbol presents parameter holder ("?") in statement
|
|
// it can be any value as long as it is not repeated with other tokens.
|
|
genericSymbol = -1
|
|
// genericSymbolList presents parameter holder lists ("?, ?, ...") in statement
|
|
// it can be any value as long as it is not repeated with other tokens.
|
|
genericSymbolList = -2
|
|
)
|
|
|
|
func (d *sqlDigester) normalize(sql string, redact string, keepHint bool, forBinding bool, forPlanReplayerReload bool) {
|
|
d.lexer.reset(sql)
|
|
d.lexer.setKeepHint(keepHint)
|
|
for {
|
|
tok, pos, lit := d.lexer.scan()
|
|
if tok == invalid {
|
|
break
|
|
}
|
|
if pos.Offset == len(sql) || (pos.Offset == len(sql)-1 && sql[pos.Offset] == ';') {
|
|
break
|
|
}
|
|
currTok := token{tok, strings.ToLower(lit)}
|
|
|
|
if !keepHint && d.reduceOptimizerHint(&currTok) {
|
|
continue
|
|
}
|
|
|
|
d.reduceLit(&currTok, redact, forBinding, forPlanReplayerReload)
|
|
if forPlanReplayerReload {
|
|
// Apply for plan replayer to match specific rules, changing IN (...) to IN (?). This can avoid plan replayer load failures caused by parse errors.
|
|
d.replaceSingleLiteralWithInList(&currTok)
|
|
} else if forBinding {
|
|
// Apply binding matching specific rules, IN (?) => IN ( ... ) #44298
|
|
d.reduceInListWithSingleLiteral(&currTok)
|
|
// In (Row(...)) => In (...) #51222
|
|
d.reduceInRowListWithSingleLiteral(&currTok)
|
|
}
|
|
|
|
if currTok.tok == identifier {
|
|
if strings.HasPrefix(currTok.lit, "_") {
|
|
_, err := charset.GetCharsetInfo(currTok.lit[1:])
|
|
if err == nil {
|
|
currTok.tok = underscoreCS
|
|
goto APPEND
|
|
}
|
|
}
|
|
|
|
if tok1 := d.lexer.isTokenIdentifier(currTok.lit, pos.Offset); tok1 != 0 {
|
|
currTok.tok = tok1
|
|
}
|
|
}
|
|
APPEND:
|
|
d.tokens.pushBack(currTok)
|
|
}
|
|
d.lexer.reset("")
|
|
for i, token := range d.tokens {
|
|
if i > 0 {
|
|
d.buffer.WriteRune(' ')
|
|
}
|
|
if token.tok == singleAtIdentifier {
|
|
d.buffer.WriteString("@")
|
|
d.buffer.WriteString(token.lit)
|
|
} else if token.tok == underscoreCS {
|
|
d.buffer.WriteString("(_charset)")
|
|
} else if token.tok == identifier || token.tok == quotedIdentifier {
|
|
d.buffer.WriteByte('`')
|
|
d.buffer.WriteString(token.lit)
|
|
d.buffer.WriteByte('`')
|
|
} else {
|
|
d.buffer.WriteString(token.lit)
|
|
}
|
|
}
|
|
d.tokens.reset()
|
|
}
|
|
|
|
func (d *sqlDigester) reduceOptimizerHint(tok *token) (reduced bool) {
|
|
// ignore /*+..*/
|
|
if tok.tok == hintComment {
|
|
return
|
|
}
|
|
|
|
// ignore force/use/ignore index(x)
|
|
if tok.lit == "index" {
|
|
toks := d.tokens.back(1)
|
|
if len(toks) > 0 {
|
|
switch strings.ToLower(toks[0].lit) {
|
|
case "force", "use", "ignore":
|
|
for {
|
|
tok, _, lit := d.lexer.scan()
|
|
if (tok == 0 && d.lexer.r.eof()) || tok == invalid {
|
|
break
|
|
}
|
|
if lit == ")" {
|
|
reduced = true
|
|
d.tokens.popBack(1)
|
|
break
|
|
}
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// ignore straight_join
|
|
if tok.lit == "straight_join" {
|
|
tok.lit = "join"
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
func (d *sqlDigester) reduceLit(currTok *token, redact string, forBinding bool, forPlanReplayer bool) {
|
|
if !d.isLit(*currTok) {
|
|
return
|
|
}
|
|
|
|
if redact == errors.RedactLogMarker && !forBinding && !forPlanReplayer {
|
|
switch currTok.lit {
|
|
case "?", "*":
|
|
return
|
|
}
|
|
input := currTok.lit
|
|
b := &strings.Builder{}
|
|
b.Grow(len(input))
|
|
_, _ = b.WriteRune('‹')
|
|
for _, c := range input {
|
|
if c == '‹' || c == '›' {
|
|
_, _ = b.WriteRune(c)
|
|
_, _ = b.WriteRune(c)
|
|
} else {
|
|
_, _ = b.WriteRune(c)
|
|
}
|
|
}
|
|
_, _ = b.WriteRune('›')
|
|
currTok.lit = b.String()
|
|
return
|
|
}
|
|
|
|
// count(*) => count(?)
|
|
if currTok.lit == "*" {
|
|
if d.isStarParam() {
|
|
currTok.tok = genericSymbol
|
|
currTok.lit = "?"
|
|
}
|
|
return
|
|
}
|
|
|
|
// "-x" or "+x" => "x"
|
|
if d.isPrefixByUnary(currTok.tok) {
|
|
d.tokens.popBack(1)
|
|
}
|
|
|
|
// "?, ?, ?, ?" => "..."
|
|
last2 := d.tokens.back(2)
|
|
if d.isGenericList(last2) {
|
|
d.tokens.popBack(2)
|
|
currTok.tok = genericSymbolList
|
|
currTok.lit = "..."
|
|
return
|
|
}
|
|
|
|
// "_charset ?, _charset ?," => "..."
|
|
last4 := d.tokens.back(4)
|
|
if toPop := d.isGenericListWithCharset(last4); toPop != 0 {
|
|
d.tokens.popBack(toPop)
|
|
currTok.tok = genericSymbolList
|
|
currTok.lit = "..."
|
|
return
|
|
}
|
|
|
|
// Aggressive reduce lists.
|
|
if d.isGenericLists(last4) {
|
|
d.tokens.popBack(4)
|
|
currTok.tok = genericSymbolList
|
|
currTok.lit = "..."
|
|
return
|
|
}
|
|
// reduce "In (row(...), row(...))" to "In (row(...))"
|
|
// final, it will be reduced to "In (...)". Issue: #51222
|
|
if forBinding {
|
|
last9 := d.tokens.back(9)
|
|
if d.isGenericRowListsWithIn(last9) {
|
|
d.tokens.popBack(5)
|
|
currTok.tok = genericSymbolList
|
|
currTok.lit = "..."
|
|
return
|
|
}
|
|
}
|
|
|
|
// order by n => order by n
|
|
if currTok.tok == intLit {
|
|
if d.isOrderOrGroupBy() {
|
|
return
|
|
}
|
|
}
|
|
|
|
// 2 => ?
|
|
currTok.tok = genericSymbol
|
|
currTok.lit = "?"
|
|
}
|
|
|
|
func (d *sqlDigester) isGenericLists(last4 []token) bool {
|
|
if len(last4) < 4 {
|
|
return false
|
|
}
|
|
if !(last4[0].tok == genericSymbol || last4[0].tok == genericSymbolList) {
|
|
return false
|
|
}
|
|
if last4[1].lit != ")" {
|
|
return false
|
|
}
|
|
if !d.isComma(last4[2]) {
|
|
return false
|
|
}
|
|
if last4[3].lit != "(" {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// In (Row(...), Row(...)) => In (Row(...))
|
|
func (d *sqlDigester) isGenericRowListsWithIn(last9 []token) bool {
|
|
if len(last9) < 7 {
|
|
return false
|
|
}
|
|
if !d.isInKeyword(last9[0]) {
|
|
return false
|
|
}
|
|
if last9[1].lit != "(" {
|
|
return false
|
|
}
|
|
if !d.isRowKeyword(last9[2]) {
|
|
return false
|
|
}
|
|
if last9[3].lit != "(" {
|
|
return false
|
|
}
|
|
if !(last9[4].tok == genericSymbol || last9[4].tok == genericSymbolList) {
|
|
return false
|
|
}
|
|
if last9[5].lit != ")" {
|
|
return false
|
|
}
|
|
if !d.isComma(last9[6]) {
|
|
return false
|
|
}
|
|
if !d.isRowKeyword(last9[7]) {
|
|
return false
|
|
}
|
|
if last9[8].lit != "(" {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// IN (...) => IN (?) Issue: #43192
|
|
func (d *sqlDigester) replaceSingleLiteralWithInList(currTok *token) {
|
|
last5 := d.tokens.back(5)
|
|
if len(last5) == 5 &&
|
|
d.isInKeyword(last5[0]) &&
|
|
d.isLeftParen(last5[1]) &&
|
|
last5[2].lit == "." &&
|
|
last5[3].lit == "." &&
|
|
last5[4].lit == "." &&
|
|
d.isRightParen(*currTok) {
|
|
d.tokens.popBack(3)
|
|
d.tokens.pushBack(token{genericSymbol, "?"})
|
|
return
|
|
}
|
|
}
|
|
|
|
// IN (?) => IN (...) Issue: #44298
|
|
func (d *sqlDigester) reduceInListWithSingleLiteral(currTok *token) {
|
|
last3 := d.tokens.back(3)
|
|
if len(last3) == 3 &&
|
|
d.isInKeyword(last3[0]) &&
|
|
d.isLeftParen(last3[1]) &&
|
|
last3[2].tok == genericSymbol &&
|
|
d.isRightParen(*currTok) {
|
|
d.tokens.popBack(1)
|
|
d.tokens.pushBack(token{genericSymbolList, "..."})
|
|
return
|
|
}
|
|
}
|
|
|
|
// In (Row(...)) => In (...) #51222
|
|
func (d *sqlDigester) reduceInRowListWithSingleLiteral(currTok *token) {
|
|
last5 := d.tokens.back(6)
|
|
if len(last5) == 6 &&
|
|
d.isInKeyword(last5[0]) &&
|
|
d.isLeftParen(last5[1]) &&
|
|
d.isRowKeyword(last5[2]) &&
|
|
d.isLeftParen(last5[3]) &&
|
|
(last5[4].tok == genericSymbolList || last5[4].tok == genericSymbol) &&
|
|
d.isRightParen(last5[5]) &&
|
|
d.isRightParen(*currTok) {
|
|
d.tokens.popBack(4)
|
|
d.tokens.pushBack(token{genericSymbolList, "..."})
|
|
return
|
|
}
|
|
}
|
|
|
|
func (d *sqlDigester) isPrefixByUnary(currTok int) (isUnary bool) {
|
|
if !d.isNumLit(currTok) {
|
|
return
|
|
}
|
|
last := d.tokens.back(1)
|
|
if last == nil {
|
|
return
|
|
}
|
|
// a[0] != '-' and a[0] != '+'
|
|
if last[0].lit != "-" && last[0].lit != "+" {
|
|
return
|
|
}
|
|
last2 := d.tokens.back(2)
|
|
if last2 == nil {
|
|
isUnary = true
|
|
return
|
|
}
|
|
// '(-x' or ',-x' or ',+x' or '--x' or '+-x'
|
|
switch last2[0].lit {
|
|
case "(", ",", "+", "-", ">=", "is", "<=", "=", "<", ">":
|
|
isUnary = true
|
|
default:
|
|
}
|
|
// select -x or select +x
|
|
last2Lit := strings.ToLower(last2[0].lit)
|
|
if last2Lit == "select" {
|
|
isUnary = true
|
|
}
|
|
return
|
|
}
|
|
|
|
func (d *sqlDigester) isGenericList(last2 []token) (generic bool) {
|
|
if len(last2) < 2 {
|
|
return false
|
|
}
|
|
if !d.isComma(last2[1]) {
|
|
return false
|
|
}
|
|
switch last2[0].tok {
|
|
case genericSymbol, genericSymbolList:
|
|
generic = true
|
|
default:
|
|
}
|
|
return
|
|
}
|
|
|
|
func (d *sqlDigester) isGenericListWithCharset(last []token) int {
|
|
if len(last) < 3 {
|
|
return 0
|
|
}
|
|
toPop := 0
|
|
if len(last) >= 4 {
|
|
// elminate the first _charset
|
|
if last[0].tok == underscoreCS {
|
|
toPop = 1
|
|
}
|
|
last = last[1:]
|
|
}
|
|
if last[2].tok != underscoreCS {
|
|
return 0
|
|
}
|
|
if !d.isGenericList(last[:2]) {
|
|
return 0
|
|
}
|
|
return toPop + 3
|
|
}
|
|
|
|
func (d *sqlDigester) isOrderOrGroupBy() (orderOrGroupBy bool) {
|
|
var (
|
|
last []token
|
|
n int
|
|
)
|
|
// skip number item lists, e.g. "order by 1, 2, 3" should NOT convert to "order by ?, ?, ?"
|
|
for n = 2; ; n += 2 {
|
|
last = d.tokens.back(n)
|
|
if len(last) < 2 {
|
|
return false
|
|
}
|
|
if !d.isComma(last[1]) {
|
|
break
|
|
}
|
|
}
|
|
// handle group by number item list surround by "()", e.g. "group by (1, 2)" should not convert to "group by (?, ?)"
|
|
if last[1].lit == "(" {
|
|
last = d.tokens.back(n + 1)
|
|
if len(last) < 2 {
|
|
return false
|
|
}
|
|
}
|
|
orderOrGroupBy = (last[0].lit == "order" || last[0].lit == "group") && last[1].lit == "by"
|
|
return
|
|
}
|
|
|
|
func (d *sqlDigester) isStarParam() (starParam bool) {
|
|
last := d.tokens.back(1)
|
|
if last == nil {
|
|
starParam = false
|
|
return
|
|
}
|
|
starParam = last[0].lit == "("
|
|
return
|
|
}
|
|
|
|
func (d *sqlDigester) isLit(t token) (beLit bool) {
|
|
tok := t.tok
|
|
if d.isNumLit(tok) || tok == stringLit || tok == bitLit || tok == paramMarker {
|
|
beLit = true
|
|
} else if t.lit == "*" {
|
|
beLit = true
|
|
} else if tok == null || (tok == identifier && strings.ToLower(t.lit) == "null") {
|
|
beLit = true
|
|
}
|
|
return
|
|
}
|
|
|
|
func (*sqlDigester) isNumLit(tok int) (beNum bool) {
|
|
switch tok {
|
|
case intLit, decLit, floatLit, hexLit:
|
|
beNum = true
|
|
default:
|
|
}
|
|
return
|
|
}
|
|
|
|
func (*sqlDigester) isComma(tok token) (isComma bool) {
|
|
isComma = tok.lit == ","
|
|
return
|
|
}
|
|
|
|
func (*sqlDigester) isLeftParen(tok token) (isLeftParen bool) {
|
|
isLeftParen = tok.lit == "("
|
|
return
|
|
}
|
|
|
|
func (*sqlDigester) isRightParen(tok token) (isLeftParen bool) {
|
|
isLeftParen = tok.lit == ")"
|
|
return
|
|
}
|
|
|
|
func (*sqlDigester) isInKeyword(tok token) (isInKeyword bool) {
|
|
isInKeyword = tok.lit == "in"
|
|
return
|
|
}
|
|
|
|
func (*sqlDigester) isRowKeyword(tok token) (isRowKeyword bool) {
|
|
isRowKeyword = tok.lit == "row"
|
|
return
|
|
}
|
|
|
|
type token struct {
|
|
tok int
|
|
lit string
|
|
}
|
|
|
|
type tokenDeque []token
|
|
|
|
func (s *tokenDeque) reset() {
|
|
*s = (*s)[:0]
|
|
}
|
|
|
|
func (s *tokenDeque) pushBack(t token) {
|
|
*s = append(*s, t)
|
|
}
|
|
|
|
func (s *tokenDeque) popBack(n int) (t []token) {
|
|
if len(*s) < n {
|
|
t = nil
|
|
return
|
|
}
|
|
t = (*s)[len(*s)-n:]
|
|
*s = (*s)[:len(*s)-n]
|
|
return
|
|
}
|
|
|
|
func (s *tokenDeque) back(n int) (t []token) {
|
|
if len(*s)-n < 0 {
|
|
return
|
|
}
|
|
t = (*s)[len(*s)-n:]
|
|
return
|
|
}
|