diff --git a/src/query/graphite/common/aliasing.go b/src/query/graphite/common/aliasing.go index 872b24e552..1f0c7f1b67 100644 --- a/src/query/graphite/common/aliasing.go +++ b/src/query/graphite/common/aliasing.go @@ -24,7 +24,6 @@ import ( "fmt" "regexp" "strconv" - "strings" "github.com/m3db/m3/src/query/graphite/ts" "github.com/m3db/m3/src/x/errors" @@ -45,19 +44,6 @@ func Alias(_ *Context, series ts.SeriesList, a string) (ts.SeriesList, error) { return series, nil } -// AliasByMetric takes a seriesList and applies an alias derived from the base -// metric name. -func AliasByMetric(ctx *Context, series ts.SeriesList) (ts.SeriesList, error) { - renamed := make([]*ts.Series, series.Len()) - for i, s := range series.Values { - firstPart := strings.Split(s.Name(), ",")[0] - terms := strings.Split(firstPart, ".") - renamed[i] = s.RenamedTo(terms[len(terms)-1]) - } - series.Values = renamed - return series, nil -} - // AliasSub runs series names through a regex search/replace. func AliasSub(_ *Context, input ts.SeriesList, search, replace string) (ts.SeriesList, error) { regex, err := regexp.Compile(search) diff --git a/src/query/graphite/native/alias_functions.go b/src/query/graphite/native/alias_functions.go index 60cb44e3d2..b64c3ae571 100644 --- a/src/query/graphite/native/alias_functions.go +++ b/src/query/graphite/native/alias_functions.go @@ -35,8 +35,8 @@ func alias(ctx *common.Context, series singlePathSpec, a string) (ts.SeriesList, // aliasByMetric takes a seriesList and applies an alias derived from the base // metric name. -func aliasByMetric(ctx *common.Context, series singlePathSpec) (ts.SeriesList, error) { - return common.AliasByMetric(ctx, ts.SeriesList(series)) +func aliasByMetric(ctx *common.Context, seriesList singlePathSpec) (ts.SeriesList, error) { + return aliasByNode(ctx, seriesList, -1) } // aliasByNode renames a time series result according to a subset of the nodes @@ -44,7 +44,10 @@ func aliasByMetric(ctx *common.Context, series singlePathSpec) (ts.SeriesList, e func aliasByNode(ctx *common.Context, seriesList singlePathSpec, nodes ...int) (ts.SeriesList, error) { renamed := make([]*ts.Series, 0, ts.SeriesList(seriesList).Len()) for _, series := range seriesList.Values { - name := getFirstPathExpression(series.Name()) + name, err := getFirstPathExpression(series.Name()) + if err != nil { + return ts.SeriesList{}, err + } nameParts := strings.Split(name, ".") newNameParts := make([]string, 0, len(nodes)) @@ -66,34 +69,35 @@ func aliasByNode(ctx *common.Context, seriesList singlePathSpec, nodes ...int) ( return ts.SeriesList(seriesList), nil } -func getFirstPathExpression(name string) string { - expr, err := Compile(name, CompileOptions{}) +func getFirstPathExpression(name string) (string, error) { + node, err := ParseGrammar(name, CompileOptions{}) if err != nil { - return name + return "", err } - if path, ok := getFirstPathExpressionDepthFirst(expr.Arguments()); ok { - return path + if path, ok := getFirstPathExpressionDepthFirst(node); ok { + return path, nil } - return name + return name, nil } -func getFirstPathExpressionDepthFirst(args []ArgumentASTNode) (string, bool) { - for i := 0; i < len(args); i++ { - path, ok := args[i].PathExpression() - if ok { - return path, true - } +func getFirstPathExpressionDepthFirst(node ASTNode) (string, bool) { + path, ok := node.PathExpression() + if ok { + return path, true + } - inner, ok := args[i].(CallASTNode) - if !ok { - continue - } + call, ok := node.CallExpression() + if !ok { + return "", false + } - path, ok = getFirstPathExpressionDepthFirst(inner.Arguments()) + for _, arg := range call.Arguments() { + path, ok = getFirstPathExpressionDepthFirst(arg) if ok { return path, true } } + return "", false } diff --git a/src/query/graphite/native/alias_functions_test.go b/src/query/graphite/native/alias_functions_test.go index a71e4e968f..1e2e64e969 100644 --- a/src/query/graphite/native/alias_functions_test.go +++ b/src/query/graphite/native/alias_functions_test.go @@ -47,8 +47,7 @@ func TestAlias(t *testing.T) { results, err := alias(nil, singlePathSpec{ Values: series, }, a) - require.Nil(t, err) - require.NotNil(t, results) + require.NoError(t, err) require.Equal(t, len(series), results.Len()) for _, s := range results.Values { assert.Equal(t, a, s.Name()) @@ -145,14 +144,13 @@ func TestAliasByMetric(t *testing.T) { ts.NewSeries(ctx, "foo.bar.baz.foo01-foo.writes.success", now, values), ts.NewSeries(ctx, "foo.bar.baz.foo02-foo.writes.success.P99", now, values), ts.NewSeries(ctx, "foo.bar.baz.foo03-foo.writes.success.P75", now, values), - ts.NewSeries(ctx, "scale(stats.foobar.gauges.quazqux.latency_minutes.foo, 60.123))", now, values), + ts.NewSeries(ctx, "scale(stats.foobar.gauges.quazqux.latency_minutes.foo, 60.123)", now, values), } results, err := aliasByMetric(ctx, singlePathSpec{ Values: series, }) - require.Nil(t, err) - require.NotNil(t, results) + require.NoError(t, err) require.Equal(t, len(series), len(results.Values)) assert.Equal(t, "success", results.Values[0].Name()) assert.Equal(t, "P99", results.Values[1].Name()) @@ -176,8 +174,7 @@ func TestAliasByNode(t *testing.T) { results, err := aliasByNode(ctx, singlePathSpec{ Values: series, }, 3, 5, 6) - require.Nil(t, err) - require.NotNil(t, results) + require.NoError(t, err) require.Equal(t, len(series), results.Len()) assert.Equal(t, "foo01-foo.success", results.Values[0].Name()) assert.Equal(t, "foo02-foo.success.P99", results.Values[1].Name()) @@ -186,8 +183,7 @@ func TestAliasByNode(t *testing.T) { results, err = aliasByNode(nil, singlePathSpec{ Values: series, }, -1) - require.Nil(t, err) - require.NotNil(t, results) + require.NoError(t, err) require.Equal(t, len(series), results.Len()) assert.Equal(t, "success", results.Values[0].Name()) assert.Equal(t, "P99", results.Values[1].Name()) @@ -203,18 +199,17 @@ func TestAliasByNodeWithComposition(t *testing.T) { series := []*ts.Series{ ts.NewSeries(ctx, "derivative(servers.bob02-foo.cpu.load_5)", now, values), ts.NewSeries(ctx, "derivative(derivative(servers.bob02-foo.cpu.load_5))", now, values), - ts.NewSeries(ctx, "~~~", now, values), + ts.NewSeries(ctx, "fooble", now, values), ts.NewSeries(ctx, "", now, values), } results, err := aliasByNode(ctx, singlePathSpec{ Values: series, }, 0, 1) - require.Nil(t, err) - require.NotNil(t, results) + require.NoError(t, err) require.Equal(t, len(series), results.Len()) assert.Equal(t, "servers.bob02-foo", results.Values[0].Name()) assert.Equal(t, "servers.bob02-foo", results.Values[1].Name()) - assert.Equal(t, "~~~", results.Values[2].Name()) + assert.Equal(t, "fooble", results.Values[2].Name()) assert.Equal(t, "", results.Values[3].Name()) } @@ -231,9 +226,27 @@ func TestAliasByNodeWithManyPathExpressions(t *testing.T) { results, err := aliasByNode(ctx, singlePathSpec{ Values: series, }, 0, 1) - require.Nil(t, err) - require.NotNil(t, results) + require.NoError(t, err) require.Equal(t, len(series), results.Len()) assert.Equal(t, "servers.bob02-foo", results.Values[0].Name()) assert.Equal(t, "servers.bob04-foo", results.Values[1].Name()) } + +func TestAliasByNodeWitCallSubExpressions(t *testing.T) { + ctx := common.NewTestContext() + defer func() { _ = ctx.Close() }() + + now := time.Now() + values := ts.NewConstantValues(ctx, 10.0, 1000, 10) + series := []*ts.Series{ + ts.NewSeries(ctx, "asPercent(foo01,sumSeries(bar,baz))", now, values), + ts.NewSeries(ctx, "asPercent(foo02,sumSeries(bar,baz))", now, values), + } + results, err := aliasByNode(ctx, singlePathSpec{ + Values: series, + }, 0) + require.NoError(t, err) + require.Equal(t, len(series), results.Len()) + assert.Equal(t, "foo01", results.Values[0].Name()) + assert.Equal(t, "foo02", results.Values[1].Name()) +} diff --git a/src/query/graphite/native/builtin_functions.go b/src/query/graphite/native/builtin_functions.go index 059dcf38d2..a782f12cdb 100644 --- a/src/query/graphite/native/builtin_functions.go +++ b/src/query/graphite/native/builtin_functions.go @@ -1663,6 +1663,7 @@ func safeIndex(len, index int) int { // of the array (if only one integer n is passed) or n - m elements of the array (if two integers n and m // are passed). func substr(_ *common.Context, seriesList singlePathSpec, start, stop int) (ts.SeriesList, error) { + origStart, origStop := start, stop results := make([]*ts.Series, len(seriesList.Values)) re := regexp.MustCompile(",.*$") for i, series := range seriesList.Values { @@ -1676,18 +1677,26 @@ func substr(_ *common.Context, seriesList singlePathSpec, start, stop int) (ts.S right = safeIndex(length, right) nameParts := strings.Split(name[left:right], ".") numParts := len(nameParts) + currStart, currStop := start, stop + // Graphite supports negative indexing, so we need to also. + if currStart < 0 { + currStart += numParts + } + if currStop < 0 { + currStop += numParts + } // If stop == 0, it's as if stop was unspecified - if start < 0 || start >= numParts || (stop != 0 && stop < start) { + if currStart < 0 || currStart >= numParts || (currStop != 0 && currStop < currStart) { err := xerrors.NewInvalidParamsError(fmt.Errorf( - "invalid substr params, start=%d, stop=%d", start, stop)) + "invalid substr params: start=%d, stop=%d", origStart, origStop)) return ts.NewSeriesList(), err } var newName string - if stop == 0 { - newName = strings.Join(nameParts[start:], ".") + if currStop == 0 { + newName = strings.Join(nameParts[currStart:], ".") } else { - stop = safeIndex(numParts, stop) - newName = strings.Join(nameParts[start:stop], ".") + stop = safeIndex(numParts, currStop) + newName = strings.Join(nameParts[currStart:currStop], ".") } newName = re.ReplaceAllString(newName, "") results[i] = series.RenamedTo(newName) diff --git a/src/query/graphite/native/builtin_functions_test.go b/src/query/graphite/native/builtin_functions_test.go index 94f00a9ef0..10584354ae 100644 --- a/src/query/graphite/native/builtin_functions_test.go +++ b/src/query/graphite/native/builtin_functions_test.go @@ -3244,7 +3244,7 @@ func TestSubstr(t *testing.T) { Values: []*ts.Series{series}, }, 1, 0) expected := common.TestSeries{Name: "bar", Data: input.values} - require.Nil(t, err) + require.NoError(t, err) common.CompareOutputsAndExpected(t, input.stepInMilli, input.startTime, []common.TestSeries{expected}, results.Values) @@ -3252,7 +3252,7 @@ func TestSubstr(t *testing.T) { Values: []*ts.Series{series}, }, 0, 2) expected = common.TestSeries{Name: "foo.bar", Data: input.values} - require.Nil(t, err) + require.NoError(t, err) common.CompareOutputsAndExpected(t, input.stepInMilli, input.startTime, []common.TestSeries{expected}, results.Values) @@ -3260,24 +3260,28 @@ func TestSubstr(t *testing.T) { Values: []*ts.Series{series}, }, 0, 0) expected = common.TestSeries{Name: "foo.bar", Data: input.values} - require.Nil(t, err) + require.NoError(t, err) common.CompareOutputsAndExpected(t, input.stepInMilli, input.startTime, []common.TestSeries{expected}, results.Values) + // Negative support -1, 0. results, err = substr(ctx, singlePathSpec{ Values: []*ts.Series{series}, - }, 2, 1) - require.NotNil(t, err) + }, -1, 0) + expected = common.TestSeries{Name: "bar", Data: input.values} + require.NoError(t, err) + common.CompareOutputsAndExpected(t, input.stepInMilli, input.startTime, + []common.TestSeries{expected}, results.Values) results, err = substr(ctx, singlePathSpec{ Values: []*ts.Series{series}, - }, -1, 1) - require.NotNil(t, err) + }, 2, 1) + require.Error(t, err) results, err = substr(ctx, singlePathSpec{ Values: []*ts.Series{series}, }, 3, 4) - require.NotNil(t, err) + require.Error(t, err) } type mockStorage struct{} diff --git a/src/query/graphite/native/compiler.go b/src/query/graphite/native/compiler.go index f2ae1cc1ab..588bf88c73 100644 --- a/src/query/graphite/native/compiler.go +++ b/src/query/graphite/native/compiler.go @@ -21,6 +21,7 @@ package native import ( + goerrors "errors" "fmt" "math" "reflect" @@ -37,24 +38,43 @@ type CompileOptions struct { // Compile converts an input stream into the corresponding Expression. func Compile(input string, opts CompileOptions) (Expression, error) { + compiler, closer := newCompiler(input, opts) + defer closer() + return compiler.compileExpression() +} + +// ParseGrammar parses the grammar into a set of AST nodes and allows +// for functions to not exist, etc. +func ParseGrammar(input string, opts CompileOptions) (ASTNode, error) { + compiler, closer := newCompiler(input, opts) + defer closer() + return compiler.parseGrammar() +} + +type closer func() + +func newCompiler(input string, opts CompileOptions) (*compiler, closer) { booleanLiterals := map[string]lexer.TokenType{ "true": lexer.True, "false": lexer.False, } + lex, tokens := lexer.NewLexer(input, booleanLiterals, lexer.Options{ EscapeAllNotOnlyQuotes: opts.EscapeAllNotOnlyQuotes, }) - go lex.Run() - lookforward := newTokenLookforward(tokens) - c := compiler{input: input, tokens: lookforward} - expr, err := c.compileExpression() + go lex.Run() - // Exhaust all tokens until closed or else lexer won't close - for range tokens { - } + cleanup := closer(func() { + // Exhaust all tokens until closed or else lexer won't close. + for range tokens { + } + }) - return expr, err + return &compiler{ + input: input, + tokens: newTokenLookforward(tokens), + }, cleanup } type tokenLookforward struct { @@ -115,11 +135,11 @@ func (c *compiler) compileExpression() (Expression, error) { expr = newFetchExpression(token.Value()) case lexer.Identifier: - fc, err := c.compileFunctionCall(token.Value(), nil) + fc, err := c.compileFunctionCall(token.Value()) fetchCandidate := false if err != nil { - _, fnNotFound := err.(errFuncNotFound) - if fnNotFound && c.canCompileAsFetch(token.Value()) { + var notFoundErr *errFuncNotFound + if goerrors.As(err, ¬FoundErr) && c.canCompileAsFetch(token.Value()) { fetchCandidate = true expr = newFetchExpression(token.Value()) } else { @@ -145,6 +165,14 @@ func (c *compiler) compileExpression() (Expression, error) { return expr, nil } +func (c *compiler) parseGrammar() (ASTNode, error) { + expr, err := c.compileExpression() + if err != nil { + return nil, err + } + return rootASTNode{expr: expr}, nil +} + // canCompileAsFetch attempts to see if the given term is a non-delimited // carbon metric; no dots, without any trailing parentheses. func (c *compiler) canCompileAsFetch(fname string) bool { @@ -157,23 +185,17 @@ func (c *compiler) canCompileAsFetch(fname string) bool { type errFuncNotFound struct{ err error } -func (e errFuncNotFound) Error() string { return e.err.Error() } +func (e *errFuncNotFound) Error() string { return e.err.Error() } // compileFunctionCall compiles a function call -func (c *compiler) compileFunctionCall(fname string, nextToken *lexer.Token) (*functionCall, error) { +func (c *compiler) compileFunctionCall(fname string) (*functionCall, error) { fn := findFunction(fname) if fn == nil { - return nil, errFuncNotFound{c.errorf("could not find function named %s", fname)} + return nil, &errFuncNotFound{c.errorf("could not find function named %s", fname)} } - if nextToken != nil { - if nextToken.TokenType() != lexer.LParenthesis { - return nil, c.errorf("expected %v but encountered %s", lexer.LParenthesis, nextToken.Value()) - } - } else { - if _, err := c.expectToken(lexer.LParenthesis); err != nil { - return nil, err - } + if _, err := c.expectToken(lexer.LParenthesis); err != nil { + return nil, err } argTypes := fn.in @@ -231,8 +253,11 @@ func (c *compiler) compileFunctionCall(fname string, nextToken *lexer.Token) (*f } // compileArg parses and compiles a single argument -func (c *compiler) compileArg(fname string, index int, - reflectType reflect.Type) (arg funcArg, foundRParen bool, err error) { +func (c *compiler) compileArg( + fname string, + index int, + reflectType reflect.Type, +) (arg funcArg, foundRParen bool, err error) { token := c.tokens.get() if token == nil { return nil, false, c.errorf("unexpected eof while parsing %s", fname) @@ -294,12 +319,14 @@ func (c *compiler) convertTokenToArg(token *lexer.Token, reflectType reflect.Typ currentToken := token.Value() // handle named arguments - nextToken := c.tokens.get() - if nextToken == nil { + nextToken, hasNextToken := c.tokens.peek() + if !hasNextToken { return nil, c.errorf("unexpected eof, %s should be followed by = or (", currentToken) } + if nextToken.TokenType() == lexer.Equal { // TODO: check if currentToken matches the expected parameter name + _ = c.tokens.get() // Consume the peeked equal token. tokenAfterNext := c.tokens.get() if tokenAfterNext == nil { return nil, c.errorf("unexpected eof, named argument %s should be followed by its value", currentToken) @@ -307,7 +334,16 @@ func (c *compiler) convertTokenToArg(token *lexer.Token, reflectType reflect.Typ return c.convertTokenToArg(tokenAfterNext, reflectType) } - return c.compileFunctionCall(currentToken, nextToken) + fc, err := c.compileFunctionCall(currentToken) + if err != nil { + var notFoundErr *errFuncNotFound + if goerrors.As(err, ¬FoundErr) && c.canCompileAsFetch(currentToken) { + return newFetchExpression(currentToken), nil + } + return nil, err + } + + return fc, nil default: return nil, c.errorf("%s not valid", token.Value()) } diff --git a/src/query/graphite/native/compiler_test.go b/src/query/graphite/native/compiler_test.go index 78c5f17860..0b5651febc 100644 --- a/src/query/graphite/native/compiler_test.go +++ b/src/query/graphite/native/compiler_test.go @@ -452,8 +452,7 @@ func TestCompileErrors(t *testing.T) { "scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, e)", "invalid expression 'scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, e)': " + "invalid function call scale, " + - "arg 1: invalid expression 'scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, e)': " + - "could not find function named e", + "arg 1: expected a float64, received 'fetch(e)'", }, { "scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, 1.2ee)", diff --git a/src/query/graphite/native/expression.go b/src/query/graphite/native/expression.go index a8b3f1ec3c..b3a7229fc7 100644 --- a/src/query/graphite/native/expression.go +++ b/src/query/graphite/native/expression.go @@ -49,15 +49,18 @@ type CallASTNode interface { // Name returns the name of the call. Name() string // Arguments describe each argument that the call has, some - // arguments can be casted to an Call themselves. - Arguments() []ArgumentASTNode + // arguments that can either be a call or path expression. + Arguments() []ASTNode } -// ArgumentASTNode is an interface to help with printing the AST. -type ArgumentASTNode interface { +// ASTNode is an interface to help with printing the AST. +type ASTNode interface { // PathExpression returns the path expression and true if argument // is a path. PathExpression() (string, bool) + // CallExpression returns the call expression and true if argument + // is a call. + CallExpression() (CallASTNode, bool) // String is the pretty printed format. String() string } @@ -76,6 +79,10 @@ func (a fetchExpressionPathArg) PathExpression() (string, bool) { return a.path, true } +func (a fetchExpressionPathArg) CallExpression() (CallASTNode, bool) { + return nil, false +} + func (a fetchExpressionPathArg) String() string { return a.path } @@ -89,14 +96,18 @@ func (f *fetchExpression) Name() string { return "fetch" } -func (f *fetchExpression) Arguments() []ArgumentASTNode { - return []ArgumentASTNode{f.pathArg} +func (f *fetchExpression) Arguments() []ASTNode { + return []ASTNode{f.pathArg} } func (f *fetchExpression) PathExpression() (string, bool) { return "", false } +func (f *fetchExpression) CallExpression() (CallASTNode, bool) { + return f, true +} + // Execute fetches results from storage func (f *fetchExpression) Execute(ctx *common.Context) (ts.SeriesList, error) { begin := time.Now() @@ -172,7 +183,7 @@ func (f *funcExpression) Name() string { return f.call.Name() } -func (f *funcExpression) Arguments() []ArgumentASTNode { +func (f *funcExpression) Arguments() []ASTNode { return f.call.Arguments() } @@ -188,6 +199,8 @@ func (f *funcExpression) Execute(ctx *common.Context) (ts.SeriesList, error) { func (f *funcExpression) String() string { return f.call.String() } +var _ ASTNode = noopExpression{} + // A noopExpression is an empty expression that returns nothing type noopExpression struct{} @@ -200,10 +213,46 @@ func (noop noopExpression) Name() string { return "noop" } -func (noop noopExpression) Arguments() []ArgumentASTNode { +func (noop noopExpression) Arguments() []ASTNode { return nil } func (noop noopExpression) String() string { return noop.Name() } + +func (noop noopExpression) PathExpression() (string, bool) { + return "", false +} + +func (noop noopExpression) CallExpression() (CallASTNode, bool) { + return noop, true +} + +var _ ASTNode = rootASTNode{} + +// A rootASTNode is the root AST node which returns child nodes +// when parsing the grammar. +type rootASTNode struct { + expr Expression +} + +func (r rootASTNode) Name() string { + return r.expr.Name() +} + +func (r rootASTNode) Arguments() []ASTNode { + return r.expr.Arguments() +} + +func (r rootASTNode) String() string { + return r.expr.(ASTNode).String() +} + +func (r rootASTNode) PathExpression() (string, bool) { + return "", false +} + +func (r rootASTNode) CallExpression() (CallASTNode, bool) { + return r.expr, true +} diff --git a/src/query/graphite/native/functions.go b/src/query/graphite/native/functions.go index 46c47a2f70..05b9c76250 100644 --- a/src/query/graphite/native/functions.go +++ b/src/query/graphite/native/functions.go @@ -497,7 +497,7 @@ func (f *Function) reflectCall(ctx *common.Context, args []reflect.Value) (refle // A funcArg is an argument to a function that gets resolved at runtime type funcArg interface { - ArgumentASTNode + ASTNode Evaluate(ctx *common.Context) (reflect.Value, error) CompatibleWith(reflectType reflect.Type) bool } @@ -517,8 +517,9 @@ func (c constFuncArg) Evaluate(ctx *common.Context) (reflect.Value, error) { ret func (c constFuncArg) CompatibleWith(reflectType reflect.Type) bool { return c.value.Type() == reflectType || reflectType == interfaceType } -func (c constFuncArg) String() string { return fmt.Sprintf("%v", c.value.Interface()) } -func (c constFuncArg) PathExpression() (string, bool) { return "", false } +func (c constFuncArg) String() string { return fmt.Sprintf("%v", c.value.Interface()) } +func (c constFuncArg) PathExpression() (string, bool) { return "", false } +func (c constFuncArg) CallExpression() (CallASTNode, bool) { return nil, false } // A functionCall is an actual call to a function, with resolution for arguments type functionCall struct { @@ -530,10 +531,10 @@ func (call *functionCall) Name() string { return call.f.name } -func (call *functionCall) Arguments() []ArgumentASTNode { - args := make([]ArgumentASTNode, len(call.in)) - for i, arg := range call.in { - args[i] = arg +func (call *functionCall) Arguments() []ASTNode { + args := make([]ASTNode, 0, len(call.in)) + for _, arg := range call.in { + args = append(args, arg) } return args } @@ -542,6 +543,10 @@ func (call *functionCall) PathExpression() (string, bool) { return "", false } +func (call *functionCall) CallExpression() (CallASTNode, bool) { + return call, true +} + // Evaluate evaluates the function call and returns the result as a reflect.Value func (call *functionCall) Evaluate(ctx *common.Context) (reflect.Value, error) { values := make([]reflect.Value, len(call.in))