tile38/internal/server/token.go

828 lines
18 KiB
Go
Raw Permalink Normal View History

package server
2016-03-05 02:08:16 +03:00
import (
"errors"
"fmt"
"math"
"strconv"
"strings"
2016-03-28 18:57:41 +03:00
"github.com/yuin/gopher-lua"
2016-03-05 02:08:16 +03:00
)
const defaultSearchOutput = outputObjects
var errInvalidNumberOfArguments = errors.New("invalid number of arguments")
var errKeyNotFound = errors.New("key not found")
var errIDNotFound = errors.New("id not found")
2016-10-31 20:17:30 +03:00
var errIDAlreadyExists = errors.New("id already exists")
var errPathNotFound = errors.New("path not found")
2018-12-28 04:15:53 +03:00
var errKeyHasHooksSet = errors.New("key has hooks set")
2016-03-05 02:08:16 +03:00
func errInvalidArgument(arg string) error {
return fmt.Errorf("invalid argument '%s'", arg)
}
func errDuplicateArgument(arg string) error {
return fmt.Errorf("duplicate argument '%s'", arg)
}
func token(line string) (newLine, token string) {
for i := 0; i < len(line); i++ {
if line[i] == ' ' {
return line[i+1:], line[:i]
}
}
return "", line
}
func tokenval(vs []string) (nvs []string, token string, ok bool) {
2016-03-28 18:57:41 +03:00
if len(vs) > 0 {
token = vs[0]
2016-03-28 18:57:41 +03:00
nvs = vs[1:]
ok = true
}
return
}
func tokenvalbytes(vs []string) (nvs []string, token []byte, ok bool) {
if len(vs) > 0 {
token = []byte(vs[0])
nvs = vs[1:]
ok = true
}
return
}
2016-03-05 02:08:16 +03:00
func tokenlc(line string) (newLine, token string) {
for i := 0; i < len(line); i++ {
ch := line[i]
if ch == ' ' {
return line[i+1:], line[:i]
}
if ch >= 'A' && ch <= 'Z' {
lc := make([]byte, 0, 16)
if i > 0 {
lc = append(lc, []byte(line[:i])...)
}
lc = append(lc, ch+32)
i++
for ; i < len(line); i++ {
ch = line[i]
if ch == ' ' {
return line[i+1:], string(lc)
}
if ch >= 'A' && ch <= 'Z' {
lc = append(lc, ch+32)
} else {
lc = append(lc, ch)
}
}
return "", string(lc)
}
}
return "", line
}
func lcb(s1 []byte, s2 string) bool {
if len(s1) != len(s2) {
return false
}
for i := 0; i < len(s1); i++ {
ch := s1[i]
if ch >= 'A' && ch <= 'Z' {
if ch+32 != s2[i] {
return false
}
} else if ch != s2[i] {
return false
}
}
return true
}
2016-03-05 02:08:16 +03:00
func lc(s1, s2 string) bool {
if len(s1) != len(s2) {
return false
}
for i := 0; i < len(s1); i++ {
ch := s1[i]
if ch >= 'A' && ch <= 'Z' {
if ch+32 != s2[i] {
return false
}
} else if ch != s2[i] {
return false
}
}
return true
}
type whereT struct {
field string
minx bool
min float64
maxx bool
max float64
}
func (where whereT) match(value float64) bool {
if !where.minx {
if value < where.min {
return false
}
} else {
if value <= where.min {
return false
}
}
if !where.maxx {
if value > where.max {
return false
}
} else {
if value >= where.max {
return false
}
}
return true
}
2016-10-03 21:37:16 +03:00
func zMinMaxFromWheres(wheres []whereT) (minZ, maxZ float64) {
for _, w := range wheres {
if w.field == "z" {
minZ = w.min
maxZ = w.max
return
}
}
minZ = math.Inf(-1)
maxZ = math.Inf(+1)
return
}
2017-08-23 23:13:12 +03:00
type whereinT struct {
2017-10-05 18:20:40 +03:00
field string
valMap map[float64]struct{}
2017-08-23 23:13:12 +03:00
}
func (wherein whereinT) match(value float64) bool {
2017-10-05 18:20:40 +03:00
_, ok := wherein.valMap[value]
2017-08-23 23:13:12 +03:00
return ok
}
type whereevalT struct {
c *Server
2018-08-14 03:05:30 +03:00
luaState *lua.LState
fn *lua.LFunction
}
func (whereeval whereevalT) Close() {
luaSetRawGlobals(
whereeval.luaState, map[string]lua.LValue{
2018-08-14 03:05:30 +03:00
"ARGV": lua.LNil,
})
whereeval.c.luapool.Put(whereeval.luaState)
}
func (whereeval whereevalT) match(fieldsWithNames map[string]float64) bool {
fieldsTbl := whereeval.luaState.CreateTable(0, len(fieldsWithNames))
for field, val := range fieldsWithNames {
fieldsTbl.RawSetString(field, lua.LNumber(val))
}
luaSetRawGlobals(
whereeval.luaState, map[string]lua.LValue{
2018-08-14 03:05:30 +03:00
"FIELDS": fieldsTbl,
})
defer luaSetRawGlobals(
whereeval.luaState, map[string]lua.LValue{
2018-08-14 03:05:30 +03:00
"FIELDS": lua.LNil,
})
whereeval.luaState.Push(whereeval.fn)
if err := whereeval.luaState.PCall(0, 1, nil); err != nil {
panic(err.Error())
}
ret := whereeval.luaState.Get(-1)
whereeval.luaState.Pop(1)
// Make bool out of returned lua value
switch ret.Type() {
case lua.LTNil:
return false
case lua.LTBool:
return ret == lua.LTrue
case lua.LTNumber:
return float64(ret.(lua.LNumber)) != 0
case lua.LTString:
return ret.String() != ""
case lua.LTTable:
tbl := ret.(*lua.LTable)
if tbl.Len() != 0 {
return true
}
var match bool
2018-08-14 03:05:30 +03:00
tbl.ForEach(func(lk lua.LValue, lv lua.LValue) { match = true })
return match
}
panic(fmt.Sprintf("Script returned value of type %s", ret.Type()))
}
2016-03-05 02:08:16 +03:00
type searchScanBaseTokens struct {
2018-08-14 03:05:30 +03:00
key string
cursor uint64
output outputT
precision uint64
lineout string
fence bool
distance bool
2018-08-23 02:26:27 +03:00
nodwell bool
2018-08-14 03:05:30 +03:00
detect map[string]bool
accept map[string]bool
glob string
wheres []whereT
whereins []whereinT
whereevals []whereevalT
nofields bool
ulimit bool
limit uint64
usparse bool
sparse uint8
desc bool
clip bool
2016-03-05 02:08:16 +03:00
}
func (c *Server) parseSearchScanBaseTokens(
cmd string, t searchScanBaseTokens, vs []string,
2018-08-14 03:05:30 +03:00
) (
vsout []string, tout searchScanBaseTokens, err error,
2018-08-14 03:05:30 +03:00
) {
2016-03-29 00:16:21 +03:00
var ok bool
if vs, t.key, ok = tokenval(vs); !ok || t.key == "" {
2016-03-05 02:08:16 +03:00
err = errInvalidNumberOfArguments
return
}
2018-08-14 03:05:30 +03:00
fromFence := t.fence
2016-03-05 02:08:16 +03:00
var slimit string
var ssparse string
var scursor string
2016-07-12 22:18:16 +03:00
var asc bool
2016-03-05 02:08:16 +03:00
for {
2016-03-29 00:16:21 +03:00
nvs, wtok, ok := tokenval(vs)
if ok && len(wtok) > 0 {
2018-08-14 03:05:30 +03:00
switch strings.ToLower(wtok) {
case "cursor":
2016-03-29 00:16:21 +03:00
vs = nvs
2016-03-05 02:08:16 +03:00
if scursor != "" {
err = errDuplicateArgument(strings.ToUpper(wtok))
return
}
2016-03-29 00:16:21 +03:00
if vs, scursor, ok = tokenval(vs); !ok || scursor == "" {
2016-03-05 02:08:16 +03:00
err = errInvalidNumberOfArguments
return
}
continue
2018-08-14 03:05:30 +03:00
case "where":
2016-03-29 00:16:21 +03:00
vs = nvs
2016-03-05 02:08:16 +03:00
var field, smin, smax string
2016-03-29 00:16:21 +03:00
if vs, field, ok = tokenval(vs); !ok || field == "" {
2016-03-05 02:08:16 +03:00
err = errInvalidNumberOfArguments
return
}
2016-03-29 00:16:21 +03:00
if vs, smin, ok = tokenval(vs); !ok || smin == "" {
2016-03-05 02:08:16 +03:00
err = errInvalidNumberOfArguments
return
}
2016-03-29 00:16:21 +03:00
if vs, smax, ok = tokenval(vs); !ok || smax == "" {
2016-03-05 02:08:16 +03:00
err = errInvalidNumberOfArguments
return
}
var minx, maxx bool
var min, max float64
if strings.ToLower(smin) == "-inf" {
min = math.Inf(-1)
} else {
if strings.HasPrefix(smin, "(") {
minx = true
smin = smin[1:]
}
min, err = strconv.ParseFloat(smin, 64)
if err != nil {
err = errInvalidArgument(smin)
return
}
}
if strings.ToLower(smax) == "+inf" {
max = math.Inf(+1)
} else {
if strings.HasPrefix(smax, "(") {
maxx = true
smax = smax[1:]
}
max, err = strconv.ParseFloat(smax, 64)
if err != nil {
err = errInvalidArgument(smax)
return
}
}
t.wheres = append(t.wheres, whereT{field, minx, min, maxx, max})
continue
2018-08-14 03:05:30 +03:00
case "wherein":
2017-08-23 23:13:12 +03:00
vs = nvs
2017-10-05 18:20:40 +03:00
var field, nvalsStr, valStr string
2017-08-23 23:13:12 +03:00
if vs, field, ok = tokenval(vs); !ok || field == "" {
err = errInvalidNumberOfArguments
return
}
2017-10-05 18:20:40 +03:00
if vs, nvalsStr, ok = tokenval(vs); !ok || nvalsStr == "" {
2017-08-23 23:13:12 +03:00
err = errInvalidNumberOfArguments
return
}
var i, nvals uint64
2017-10-05 18:20:40 +03:00
if nvals, err = strconv.ParseUint(nvalsStr, 10, 64); err != nil {
err = errInvalidArgument(nvalsStr)
2017-08-23 23:13:12 +03:00
return
}
2017-10-05 18:20:40 +03:00
valMap := make(map[float64]struct{})
2017-08-23 23:13:12 +03:00
var val float64
var empty struct{}
for i = 0; i < nvals; i++ {
2017-10-05 18:20:40 +03:00
if vs, valStr, ok = tokenval(vs); !ok || valStr == "" {
2017-08-23 23:13:12 +03:00
err = errInvalidNumberOfArguments
return
}
2017-10-05 18:20:40 +03:00
if val, err = strconv.ParseFloat(valStr, 64); err != nil {
err = errInvalidArgument(valStr)
2017-08-23 23:13:12 +03:00
return
}
2017-10-05 18:20:40 +03:00
valMap[val] = empty
2017-08-23 23:13:12 +03:00
}
2017-10-05 18:20:40 +03:00
t.whereins = append(t.whereins, whereinT{field, valMap})
2017-08-23 23:13:12 +03:00
continue
2018-08-14 03:05:30 +03:00
case "whereevalsha":
fallthrough
case "whereeval":
scriptIsSha := strings.ToLower(wtok) == "whereevalsha"
vs = nvs
var script, nargsStr, arg string
if vs, script, ok = tokenval(vs); !ok || script == "" {
err = errInvalidNumberOfArguments
return
}
if vs, nargsStr, ok = tokenval(vs); !ok || nargsStr == "" {
err = errInvalidNumberOfArguments
return
}
var i, nargs uint64
if nargs, err = strconv.ParseUint(nargsStr, 10, 64); err != nil {
err = errInvalidArgument(nargsStr)
return
}
var luaState *lua.LState
luaState, err = c.luapool.Get()
if err != nil {
return
}
argsTbl := luaState.CreateTable(len(vs), 0)
for i = 0; i < nargs; i++ {
if vs, arg, ok = tokenval(vs); !ok || arg == "" {
err = errInvalidNumberOfArguments
return
}
argsTbl.Append(lua.LString(arg))
}
var shaSum string
if scriptIsSha {
shaSum = script
} else {
shaSum = Sha1Sum(script)
}
luaSetRawGlobals(
luaState, map[string]lua.LValue{
2018-08-14 03:05:30 +03:00
"ARGV": argsTbl,
})
compiled, ok := c.luascripts.Get(shaSum)
var fn *lua.LFunction
if ok {
fn = &lua.LFunction{
IsG: false,
Env: luaState.Env,
Proto: compiled,
GFunction: nil,
Upvalues: make([]*lua.Upvalue, 0),
}
} else if scriptIsSha {
err = errShaNotFound
return
} else {
fn, err = luaState.Load(strings.NewReader(script), "f_"+shaSum)
if err != nil {
err = makeSafeErr(err)
return
}
c.luascripts.Put(shaSum, fn.Proto)
}
2018-08-14 03:05:30 +03:00
t.whereevals = append(t.whereevals, whereevalT{c, luaState, fn})
continue
2018-08-14 03:05:30 +03:00
case "nofields":
2016-03-29 00:16:21 +03:00
vs = nvs
2016-03-05 02:08:16 +03:00
if t.nofields {
err = errDuplicateArgument(strings.ToUpper(wtok))
return
}
t.nofields = true
continue
2018-08-14 03:05:30 +03:00
case "limit":
2016-03-29 00:16:21 +03:00
vs = nvs
2016-03-05 02:08:16 +03:00
if slimit != "" {
err = errDuplicateArgument(strings.ToUpper(wtok))
return
}
2016-03-29 00:16:21 +03:00
if vs, slimit, ok = tokenval(vs); !ok || slimit == "" {
2016-03-05 02:08:16 +03:00
err = errInvalidNumberOfArguments
return
}
continue
2018-08-14 03:05:30 +03:00
case "sparse":
2016-03-29 00:16:21 +03:00
vs = nvs
2016-03-05 02:08:16 +03:00
if ssparse != "" {
err = errDuplicateArgument(strings.ToUpper(wtok))
return
}
2016-03-29 00:16:21 +03:00
if vs, ssparse, ok = tokenval(vs); !ok || ssparse == "" {
2016-03-05 02:08:16 +03:00
err = errInvalidNumberOfArguments
return
}
continue
2018-08-14 03:05:30 +03:00
case "fence":
2016-03-29 00:16:21 +03:00
vs = nvs
2018-08-14 03:05:30 +03:00
if t.fence && !fromFence {
2016-03-05 02:08:16 +03:00
err = errDuplicateArgument(strings.ToUpper(wtok))
return
}
t.fence = true
2016-04-01 22:46:39 +03:00
continue
2018-08-14 03:05:30 +03:00
case "commands":
vs = nvs
if t.accept != nil {
err = errDuplicateArgument(strings.ToUpper(wtok))
return
}
t.accept = make(map[string]bool)
var peek string
if vs, peek, ok = tokenval(vs); !ok || peek == "" {
err = errInvalidNumberOfArguments
return
}
for _, s := range strings.Split(peek, ",") {
part := strings.TrimSpace(strings.ToLower(s))
if t.accept[part] {
err = errDuplicateArgument(s)
return
}
t.accept[part] = true
}
if len(t.accept) == 0 {
t.accept = nil
}
continue
2018-08-14 03:05:30 +03:00
case "distance":
2017-01-10 19:49:48 +03:00
vs = nvs
if t.distance {
err = errDuplicateArgument(strings.ToUpper(wtok))
return
}
t.distance = true
continue
2018-08-14 03:05:30 +03:00
case "detect":
2016-04-01 22:46:39 +03:00
vs = nvs
if t.detect != nil {
err = errDuplicateArgument(strings.ToUpper(wtok))
return
}
t.detect = make(map[string]bool)
var peek string
if vs, peek, ok = tokenval(vs); !ok || peek == "" {
err = errInvalidNumberOfArguments
return
}
for _, s := range strings.Split(peek, ",") {
part := strings.TrimSpace(strings.ToLower(s))
switch part {
default:
err = errInvalidArgument(peek)
return
case "inside", "outside", "enter", "exit", "cross":
}
if t.detect[part] {
err = errDuplicateArgument(s)
return
}
t.detect[part] = true
}
if len(t.detect) == 0 {
t.detect = map[string]bool{
"inside": true,
"outside": true,
"enter": true,
"exit": true,
"cross": true,
}
}
2016-07-12 22:18:16 +03:00
continue
2018-08-23 02:26:27 +03:00
case "nodwell":
vs = nvs
if t.desc || asc {
err = errDuplicateArgument(strings.ToUpper(wtok))
return
}
t.nodwell = true
continue
2018-08-14 03:05:30 +03:00
case "desc":
2016-07-12 22:18:16 +03:00
vs = nvs
if t.desc || asc {
err = errDuplicateArgument(strings.ToUpper(wtok))
return
}
t.desc = true
continue
2018-08-14 03:05:30 +03:00
case "asc":
2016-07-12 22:18:16 +03:00
vs = nvs
if t.desc || asc {
err = errDuplicateArgument(strings.ToUpper(wtok))
return
}
asc = true
2016-03-05 02:08:16 +03:00
continue
2018-08-14 03:05:30 +03:00
case "match":
2016-03-29 00:16:21 +03:00
vs = nvs
2016-03-05 02:08:16 +03:00
if t.glob != "" {
err = errDuplicateArgument(strings.ToUpper(wtok))
return
}
2016-03-29 00:16:21 +03:00
if vs, t.glob, ok = tokenval(vs); !ok || t.glob == "" {
2016-03-05 02:08:16 +03:00
err = errInvalidNumberOfArguments
return
}
continue
2018-08-14 03:05:30 +03:00
case "clip":
2018-05-08 02:18:18 +03:00
vs = nvs
if t.clip {
err = errDuplicateArgument(strings.ToUpper(wtok))
return
}
t.clip = true
continue
2016-03-05 02:08:16 +03:00
}
}
break
}
// check to make sure that there aren't any conflicts
2016-07-13 06:11:02 +03:00
if cmd == "scan" || cmd == "search" {
2016-03-05 02:08:16 +03:00
if ssparse != "" {
2016-07-13 06:11:02 +03:00
err = errors.New("SPARSE is not allowed for " + strings.ToUpper(cmd))
2016-03-05 02:08:16 +03:00
return
}
if t.fence {
2016-07-13 06:11:02 +03:00
err = errors.New("FENCE is not allowed for " + strings.ToUpper(cmd))
2016-03-05 02:08:16 +03:00
return
}
2016-07-12 22:18:16 +03:00
} else {
if t.desc {
err = errors.New("DESC is not allowed for " + strings.ToUpper(cmd))
return
}
if asc {
err = errors.New("ASC is not allowed for " + strings.ToUpper(cmd))
return
}
2016-03-05 02:08:16 +03:00
}
if ssparse != "" && slimit != "" {
err = errors.New("LIMIT is not allowed when SPARSE is specified")
return
}
if scursor != "" && ssparse != "" {
err = errors.New("CURSOR is not allowed when SPARSE is specified")
return
}
if scursor != "" && t.fence {
err = errors.New("CURSOR is not allowed when FENCE is specified")
return
}
2016-04-01 22:46:39 +03:00
if t.detect != nil && !t.fence {
err = errors.New("DETECT is not allowed when FENCE is not specified")
return
}
2016-03-05 02:08:16 +03:00
t.output = defaultSearchOutput
var nvs []string
2016-03-05 02:08:16 +03:00
var sprecision string
var which string
2016-03-29 00:16:21 +03:00
if nvs, which, ok = tokenval(vs); ok && which != "" {
2016-03-05 02:08:16 +03:00
updline := true
switch strings.ToLower(which) {
default:
if cmd == "scan" {
err = errInvalidArgument(which)
return
}
updline = false
case "count":
t.output = outputCount
case "objects":
t.output = outputObjects
case "points":
t.output = outputPoints
case "hashes":
t.output = outputHashes
2016-03-29 00:16:21 +03:00
if nvs, sprecision, ok = tokenval(nvs); !ok || sprecision == "" {
2016-03-05 02:08:16 +03:00
err = errInvalidNumberOfArguments
return
}
case "bounds":
t.output = outputBounds
case "ids":
t.output = outputIDs
}
if updline {
2016-03-29 00:16:21 +03:00
vs = nvs
2016-03-05 02:08:16 +03:00
}
}
if scursor != "" {
if t.cursor, err = strconv.ParseUint(scursor, 10, 64); err != nil {
err = errInvalidArgument(scursor)
return
}
}
if sprecision != "" {
if t.precision, err = strconv.ParseUint(sprecision, 10, 64); err != nil || t.precision == 0 || t.precision > 64 {
err = errInvalidArgument(sprecision)
return
}
}
if slimit != "" {
t.ulimit = true
2016-03-05 02:08:16 +03:00
if t.limit, err = strconv.ParseUint(slimit, 10, 64); err != nil || t.limit == 0 {
err = errInvalidArgument(slimit)
return
}
}
if ssparse != "" {
t.usparse = true
2016-03-05 02:08:16 +03:00
var sparse uint64
if sparse, err = strconv.ParseUint(ssparse, 10, 8); err != nil || sparse == 0 || sparse > 8 {
err = errInvalidArgument(ssparse)
return
}
t.sparse = uint8(sparse)
t.limit = math.MaxUint64
}
2016-03-29 00:16:21 +03:00
vsout = vs
2018-08-14 03:05:30 +03:00
tout = t
2016-03-05 02:08:16 +03:00
return
}
type parentStack []*areaExpression
func (ps *parentStack) isEmpty() bool {
return len(*ps) == 0
}
func (ps *parentStack) push(e *areaExpression) {
*ps = append(*ps, e)
}
func (ps *parentStack) pop() (e *areaExpression, empty bool) {
n := len(*ps)
if n == 0 {
return nil, true
}
x := (*ps)[n-1]
*ps = (*ps)[:n-1]
return x, false
}
func (s *Server) parseAreaExpression(vsin []string, doClip bool) (vsout []string, ae *areaExpression, err error) {
ps := &parentStack{}
vsout = vsin[:]
var negate, needObj bool
loop:
for {
nvs, wtok, ok := tokenval(vsout)
if !ok || len(wtok) == 0 {
break
}
switch strings.ToLower(wtok) {
2019-06-12 03:13:33 +03:00
case tokenLParen:
newExpr := &areaExpression{negate: negate, op: NOOP}
negate = false
needObj = false
if ae != nil {
ae.children = append(ae.children, newExpr)
}
ae = newExpr
2019-06-13 22:04:04 +03:00
ps.push(ae)
vsout = nvs
2019-06-12 03:13:33 +03:00
case tokenRParen:
if needObj {
err = errInvalidArgument(tokenRParen)
return
}
if parent, empty := ps.pop(); empty {
2019-06-12 03:13:33 +03:00
err = errInvalidArgument(tokenRParen)
return
} else {
ae = parent
}
vsout = nvs
2019-06-12 03:13:33 +03:00
case tokenNOT:
2019-06-14 20:02:26 +03:00
negate = !negate
needObj = true
vsout = nvs
2019-06-12 03:13:33 +03:00
case tokenAND:
if needObj {
err = errInvalidArgument(tokenAND)
return
}
needObj = true
if ae == nil {
2019-06-12 03:13:33 +03:00
err = errInvalidArgument(tokenAND)
return
} else if ae.obj == nil {
switch ae.op {
case OR:
numChildren := len(ae.children)
if numChildren < 2 {
err = errInvalidNumberOfArguments
return
} else {
ae.children = append(
ae.children[:numChildren-1],
&areaExpression{
op: AND,
children: []*areaExpression{ae.children[numChildren-1]}})
}
case NOOP:
ae.op = AND
}
} else {
ae = &areaExpression{op: AND, children: []*areaExpression{ae}}
}
vsout = nvs
2019-06-12 03:13:33 +03:00
case tokenOR:
if needObj {
err = errInvalidArgument(tokenOR)
return
}
needObj = true
if ae == nil {
2019-06-12 03:13:33 +03:00
err = errInvalidArgument(tokenOR)
return
} else if ae.obj == nil {
switch ae.op {
case AND:
if len(ae.children) < 2 {
err = errInvalidNumberOfArguments
return
} else {
2019-06-13 22:04:04 +03:00
ae = &areaExpression{op: OR, children: []*areaExpression{ae}}
}
case NOOP:
ae.op = OR
}
} else {
ae = &areaExpression{op: OR, children: []*areaExpression{ae}}
}
vsout = nvs
case "point", "circle", "object", "bounds", "hash", "quadkey", "tile", "get":
if parsedVs, parsedObj, areaErr := s.parseArea(vsout, doClip); areaErr != nil {
err = areaErr
return
} else {
newExpr := &areaExpression{negate: negate, obj: parsedObj, op: NOOP}
negate = false
needObj = false
if ae == nil {
ae = newExpr
} else {
ae.children = append(ae.children, newExpr)
}
vsout = parsedVs
}
default:
break loop
}
}
if !ps.isEmpty() || needObj || ae == nil || (ae.obj == nil && len(ae.children) == 0) {
err = errInvalidNumberOfArguments
}
return
}