1
0
mirror of https://github.com/MontFerret/ferret.git synced 2026-06-20 01:17:53 +02:00

Chore/improved syntax error message (#932)

* feat: add diagnostics for missing commas between MATCH arms

* feat: enhance syntax error diagnostics for mixed function body usage

* feat: improve diagnostics for missing function parameters before function body
This commit is contained in:
Tim Voronov
2026-05-14 09:51:02 -04:00
committed by GitHub
parent c822605b2f
commit 9093d7856e
11 changed files with 687 additions and 13 deletions
+11 -5
View File
@@ -76,19 +76,25 @@ func (c *Compiler) Compile(src *source.Source) (program *bytecode.Program, err e
}
}()
l := NewVisitor(src, errorHandler, c.opts.Level)
tokenHistory := parserd.NewTokenHistory(10)
tokenHistory := parserd.NewTokenHistory(64)
p := parser.New(src.Content(), func(stream antlr.TokenStream) antlr.TokenStream {
return parserd.NewTrackingTokenStream(stream, tokenHistory)
})
// Remove all default error listeners
p.RemoveErrorListeners()
// Add custom error listener
p.AddErrorListener(parserd.NewErrorListener(src, l.Session.Program.Errors, tokenHistory))
p.AddErrorListener(parserd.NewErrorListener(src, errorHandler, tokenHistory))
p.Program()
if errorHandler.HasErrors() {
return nil, errorHandler.Unwrap()
}
l := NewVisitor(src, errorHandler, c.opts.Level)
p.Visit(l)
if l.Session.Program.Errors.HasErrors() {
return nil, l.Session.Program.Errors.Unwrap()
if errorHandler.HasErrors() {
return nil, errorHandler.Unwrap()
}
var udfs []bytecode.UDF
+7 -1
View File
@@ -59,7 +59,7 @@ func (fmt *Formatter) Format(out io.Writer, src *source.Source) error {
}
}()
tokenHistory := parserd.NewTokenHistory(10)
tokenHistory := parserd.NewTokenHistory(64)
p := parser.New(src.Content(), func(stream antlr.TokenStream) antlr.TokenStream {
return parserd.NewTrackingTokenStream(stream, tokenHistory)
})
@@ -67,6 +67,12 @@ func (fmt *Formatter) Format(out io.Writer, src *source.Source) error {
p.RemoveErrorListeners()
// Add custom error listener
p.AddErrorListener(parserd.NewErrorListener(src, errorHandler, tokenHistory))
p.Program()
if errorHandler.HasErrors() {
return errorHandler.Unwrap()
}
l := internal.NewVisitor(src, out, fmt.opts)
p.Visit(l)
+3
View File
@@ -18,6 +18,9 @@ func AnalyzeSyntaxError(src *source.Source, err *diagnostics.Diagnostic, offendi
matchWaitForErrors,
matchMissingAssignmentValue,
matchForLoopErrors,
matchMatchArmSeparatorErrors,
matchMissingFunctionParamsClose,
matchMixedFunctionBodySyntax,
matchCommonErrors,
matchMissingReturnValue,
}
+9 -3
View File
@@ -10,9 +10,10 @@ import (
type ErrorListener struct {
*antlr.DiagnosticErrorListener
src *source.Source
handler *ErrorHandler
history *TokenHistory
src *source.Source
handler *ErrorHandler
history *TokenHistory
stopAfterSyntaxError bool
}
func NewErrorListener(src *source.Source, handler *ErrorHandler, history *TokenHistory) antlr.ErrorListener {
@@ -47,6 +48,10 @@ func (d *ErrorListener) ReportAmbiguity(recognizer antlr.Parser, dfa *antlr.DFA,
}
func (d *ErrorListener) SyntaxError(_ antlr.Recognizer, offendingSymbol interface{}, line, column int, msg string, e antlr.RecognitionException) {
if d.stopAfterSyntaxError {
return
}
var offending antlr.Token
// Get offending token
@@ -57,6 +62,7 @@ func (d *ErrorListener) SyntaxError(_ antlr.Recognizer, offendingSymbol interfac
if !d.handler.HasErrorOnLine(line) {
if err := d.parseError(msg, offending); err != nil {
d.handler.Add(err)
d.stopAfterSyntaxError = isFunctionBodySyntaxDiagnostic(err)
}
}
}
+14 -1
View File
@@ -11,7 +11,7 @@ import (
func matchCommonErrors(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
if isNoAlternative(err.Message) || isMissing(err.Message) || isMismatched(err.Message) {
prev := offending.Prev()
if node := anyIs(prev, offending, "=>"); node != nil {
if node := anyIs(prev, offending, "=>"); node != nil && !isArrowBodyStart(node, offending) {
span := spanFromTokenSafe(node.Token(), src)
span.Start += 2
span.End += 2
@@ -276,6 +276,19 @@ func matchCommonErrors(src *source.Source, err *diagnostics.Diagnostic, offendin
return false
}
func isArrowBodyStart(arrow, offending *TokenNode) bool {
if arrow == nil || offending == nil || arrow == offending {
return false
}
switch offending.GetText() {
case "", "<EOF>", ",", ")", "]", "}", "=>", ":", "?":
return false
default:
return isExpressionStart(offending)
}
}
func findGroupingParen(node *TokenNode, max int) *TokenNode {
current := node
@@ -0,0 +1,175 @@
package diagnostics
import (
"github.com/antlr4-go/antlr/v4"
"github.com/MontFerret/ferret/v2/pkg/diagnostics"
"github.com/MontFerret/ferret/v2/pkg/source"
)
const (
missingFunctionParamsCloseMessage = "Expected function parameters before function body"
missingFunctionParamsCloseLabel = "missing parameter list before function body"
missingFunctionParamsCloseHint = "Add a parameter list before the block body, e.g. FUNC fib(n) ( ... RETURN expr ). Use FUNC fib() ( ... ) for no parameters."
mixedFunctionBodyMessage = "Cannot combine arrow and block function body syntax"
mixedFunctionBodyLabel = "RETURN is only valid in a block function body"
mixedFunctionBodyNote = "Remove '=>' to use a block body, or remove RETURN and keep a single expression after '=>'."
mixedFunctionBodyHint = "Use either 'FUNC f(x) => expr' or 'FUNC f(x) ( ... RETURN expr )'."
)
func matchMissingFunctionParamsClose(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
if src == nil || err == nil || offending == nil {
return false
}
if !isNoAlternative(err.Message) && !isMissing(err.Message) && !isMismatched(err.Message) {
return false
}
tokens := lexDefaultTokens(src.Content())
offendingIdx := findDiagnosticSpanTokenIndex(tokens, err)
if offendingIdx < 0 || !isFunctionBlockBodyStartToken(tokens[offendingIdx]) {
return false
}
paramsOpenIdx := findUnclosedFunctionParamsOpen(tokens, offendingIdx)
if paramsOpenIdx < 0 {
return false
}
err.Message = missingFunctionParamsCloseMessage
err.Hint = missingFunctionParamsCloseHint
err.Spans = []diagnostics.ErrorSpan{
diagnostics.NewMainErrorSpan(spanFromTokenSafe(tokens[paramsOpenIdx], src), missingFunctionParamsCloseLabel),
}
return true
}
func matchMixedFunctionBodySyntax(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
if src == nil || err == nil || offending == nil {
return false
}
if !isNoAlternative(err.Message) && !isMissing(err.Message) && !isMismatched(err.Message) {
return false
}
if !is(offending, "RETURN") {
return false
}
openParen := offending.Prev()
if !is(openParen, "(") || !is(openParen.Prev(), "=>") {
return false
}
if !isFunctionDeclarationArrow(openParen.Prev()) {
return false
}
err.Message = mixedFunctionBodyMessage
err.Hint = mixedFunctionBodyHint
err.Note = mixedFunctionBodyNote
err.Spans = []diagnostics.ErrorSpan{
diagnostics.NewMainErrorSpan(spanFromTokenSafe(offending.Token(), src), mixedFunctionBodyLabel),
}
return true
}
func isFunctionBodySyntaxDiagnostic(err *diagnostics.Diagnostic) bool {
return isMissingFunctionParamsCloseDiagnostic(err) || isMixedFunctionBodySyntaxDiagnostic(err)
}
func isMissingFunctionParamsCloseDiagnostic(err *diagnostics.Diagnostic) bool {
return err != nil && err.Kind == SyntaxError && err.Message == missingFunctionParamsCloseMessage
}
func isMixedFunctionBodySyntaxDiagnostic(err *diagnostics.Diagnostic) bool {
return err != nil && err.Kind == SyntaxError && err.Message == mixedFunctionBodyMessage
}
func isFunctionDeclarationArrow(arrow *TokenNode) bool {
if !is(arrow, "=>") {
return false
}
paramsClose := arrow.Prev()
if !is(paramsClose, ")") {
return false
}
depth := 0
for current := paramsClose; current != nil; current = current.Prev() {
switch {
case is(current, ")"):
depth++
case is(current, "("):
depth--
if depth == 0 {
name := current.Prev()
return isFunctionNameToken(name) && is(name.Prev(), "FUNC")
}
}
}
return false
}
func findDiagnosticSpanTokenIndex(tokens []antlr.Token, err *diagnostics.Diagnostic) int {
if err == nil || len(err.Spans) == 0 {
return -1
}
span := err.Spans[0].Span
for idx, token := range tokens {
if token.GetStart() == span.Start && token.GetStop()+1 == span.End {
return idx
}
}
return -1
}
func isFunctionBlockBodyStartToken(token antlr.Token) bool {
return isTokenText(token, "RETURN") ||
isTokenText(token, "LET") ||
isTokenText(token, "VAR") ||
isTokenText(token, "FUNC") ||
isTokenText(token, "WAITFOR") ||
isTokenText(token, "DISPATCH")
}
func findUnclosedFunctionParamsOpen(tokens []antlr.Token, offendingIdx int) int {
depth := 0
for i := offendingIdx - 1; i >= 0; i-- {
switch tokenText(tokens[i]) {
case ")":
depth++
case "(":
if depth > 0 {
depth--
continue
}
if i >= 2 && isTokenText(tokens[i-2], "FUNC") {
return i
}
return -1
}
}
return -1
}
func isFunctionNameToken(node *TokenNode) bool {
if node == nil || node.Token() == nil {
return false
}
return isIdentifier(node) || isKeyword(node)
}
+231
View File
@@ -0,0 +1,231 @@
package diagnostics
import (
"strings"
"github.com/antlr4-go/antlr/v4"
"github.com/MontFerret/ferret/v2/pkg/diagnostics"
"github.com/MontFerret/ferret/v2/pkg/parser/fql"
"github.com/MontFerret/ferret/v2/pkg/source"
)
const (
missingMatchArmCommaMessage = "Expected ',' between MATCH arms"
missingMatchArmCommaHint = "Separate MATCH arms with commas, e.g. 0 => 0, 1 => 1, _ => 0."
)
func matchMatchArmSeparatorErrors(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
if !(isNoAlternative(err.Message) || isMissing(err.Message) || isMismatched(err.Message)) {
return false
}
if src == nil || offending == nil || offending.Token() == nil {
return false
}
tokens := lexDefaultTokens(src.Content())
offendingIdx := findLexedTokenIndex(tokens, offending.Token())
if offendingIdx < 0 {
return false
}
for arrowIdx := 0; arrowIdx < len(tokens); arrowIdx++ {
if !isTokenText(tokens[arrowIdx], "=>") || !isInsideMatchArms(tokens, arrowIdx) {
continue
}
nextArrowIdx, ok := findNextTopLevelArrowBeforeSeparator(tokens, arrowIdx+1)
if !ok || nextArrowIdx <= arrowIdx+1 {
continue
}
if offendingIdx < arrowIdx || offendingIdx > nextArrowIdx {
continue
}
span, ok := matchArmSeparatorInsertionSpan(tokens, arrowIdx, nextArrowIdx, src)
if !ok {
continue
}
err.Message = missingMatchArmCommaMessage
err.Hint = missingMatchArmCommaHint
err.Spans = []diagnostics.ErrorSpan{
diagnostics.NewMainErrorSpan(span, "missing comma"),
}
return true
}
return false
}
func lexDefaultTokens(input string) []antlr.Token {
stream := antlr.NewInputStream(input)
lexer := fql.NewFqlLexer(stream)
lexer.RemoveErrorListeners()
tokens := make([]antlr.Token, 0)
for {
token := lexer.NextToken()
if token == nil || token.GetTokenType() == antlr.TokenEOF {
break
}
if token.GetChannel() == antlr.TokenDefaultChannel {
tokens = append(tokens, token)
}
}
return tokens
}
func findLexedTokenIndex(tokens []antlr.Token, token antlr.Token) int {
if token == nil {
return -1
}
for idx, candidate := range tokens {
if candidate.GetStart() == token.GetStart() && candidate.GetStop() == token.GetStop() {
return idx
}
}
return -1
}
func isInsideMatchArms(tokens []antlr.Token, idx int) bool {
openIdx := findEnclosingOpenParen(tokens, idx)
if openIdx < 0 {
return false
}
depth := 0
for i := openIdx - 1; i >= 0; i-- {
text := tokenText(tokens[i])
switch text {
case ")", "]", "}":
depth++
case "(", "[", "{":
if depth > 0 {
depth--
}
case "MATCH":
if depth == 0 {
return true
}
case "FUNC", "RETURN", "LET", "VAR", "FOR", "USE":
if depth == 0 {
return false
}
}
}
return false
}
func findEnclosingOpenParen(tokens []antlr.Token, idx int) int {
depth := 0
for i := idx - 1; i >= 0; i-- {
text := tokenText(tokens[i])
switch text {
case ")":
depth++
case "(":
if depth == 0 {
return i
}
depth--
}
}
return -1
}
func findNextTopLevelArrowBeforeSeparator(tokens []antlr.Token, startIdx int) (int, bool) {
depth := 0
for i := startIdx; i < len(tokens); i++ {
text := tokenText(tokens[i])
switch text {
case "(", "[", "{":
depth++
case ")", "]", "}":
if depth == 0 {
return -1, false
}
depth--
case ",":
if depth == 0 {
return -1, false
}
case "=>":
if depth == 0 {
return i, true
}
}
}
return -1, false
}
func matchArmSeparatorInsertionSpan(tokens []antlr.Token, currentArrowIdx, nextArrowIdx int, src *source.Source) (source.Span, bool) {
nextArmStartIdx := matchArmStartTokenIndex(tokens, currentArrowIdx, nextArrowIdx)
if nextArmStartIdx <= currentArrowIdx+1 {
return source.Span{}, false
}
previousValue := tokens[nextArmStartIdx-1]
if previousValue == nil {
return source.Span{}, false
}
offset := previousValue.GetStop() + 1
if offset < 0 || offset >= len(src.Content()) {
return spanFromTokenSafe(previousValue, src), true
}
return source.Span{Start: offset, End: offset + 1}, true
}
func matchArmStartTokenIndex(tokens []antlr.Token, currentArrowIdx, nextArrowIdx int) int {
depth := 0
for i := nextArrowIdx - 1; i > currentArrowIdx; i-- {
text := tokenText(tokens[i])
switch text {
case ")", "]", "}":
depth++
case "(", "[", "{":
if depth > 0 {
depth--
}
case "WHEN":
if depth == 0 {
return i
}
}
}
return nextArrowIdx - 1
}
func isTokenText(token antlr.Token, expected string) bool {
return tokenText(token) == expected
}
func tokenText(token antlr.Token) string {
if token == nil {
return ""
}
return strings.ToUpper(token.GetText())
}
+14 -3
View File
@@ -9,7 +9,8 @@ import (
)
type Parser struct {
tree *fql.FqlParser
tree *fql.FqlParser
program *fql.ProgramContext
}
func New(query string, tr ...TokenStreamTransformer) *Parser {
@@ -45,10 +46,20 @@ func (p *Parser) RemoveErrorListeners() {
p.tree.RemoveErrorListeners()
}
// Program parses and returns the source program, caching the parse tree so
// syntax diagnostics can be inspected before visitors consume it.
func (p *Parser) Program() *fql.ProgramContext {
if p.program == nil {
p.program = p.tree.Program().(*fql.ProgramContext)
}
return p.program
}
func (p *Parser) Visit(visitor fql.FqlParserVisitor) interface{} {
return visitor.VisitProgram(p.tree.Program().(*fql.ProgramContext))
return visitor.VisitProgram(p.Program())
}
func (p *Parser) Walk(listener fql.FqlParserListener) {
antlr.ParseTreeWalkerDefault.Walk(listener, p.tree.Program())
antlr.ParseTreeWalkerDefault.Walk(listener, p.Program())
}
@@ -1,9 +1,13 @@
package compiler_test
import (
"strings"
"testing"
"github.com/MontFerret/ferret/v2/pkg/compiler"
pkgdiagnostics "github.com/MontFerret/ferret/v2/pkg/diagnostics"
parserd "github.com/MontFerret/ferret/v2/pkg/parser/diagnostics"
"github.com/MontFerret/ferret/v2/pkg/source"
"github.com/MontFerret/ferret/v2/test/spec"
. "github.com/MontFerret/ferret/v2/test/spec/compile"
)
@@ -17,5 +21,93 @@ func TestMatchErrors(t *testing.T) {
Message: "duplicate binding 'v' in MATCH pattern",
},
),
Failure(
`
FUNC fib(n) (
RETURN MATCH n (
0 => 0
1 => 1
_ => fib(n - 1) + fib(n - 2)
)
)
RETURN fib(10)
`,
E{
Kind: parserd.SyntaxError,
Message: "Expected ',' between MATCH arms",
Hint: "Separate MATCH arms with commas, e.g. 0 => 0, 1 => 1, _ => 0.",
},
"Missing comma between MATCH pattern arms",
),
Failure(
`
RETURN MATCH (
WHEN TRUE => "yes"
WHEN FALSE => "no",
_ => "fallback",
)
`,
E{
Kind: parserd.SyntaxError,
Message: "Expected ',' between MATCH arms",
Hint: "Separate MATCH arms with commas, e.g. 0 => 0, 1 => 1, _ => 0.",
},
"Missing comma between MATCH guard arms",
),
Failure(
`RETURN MATCH 0 ( 1 => 1 _ => 0 )`,
E{
Kind: parserd.SyntaxError,
Message: "Expected ',' between MATCH arms",
Hint: "Separate MATCH arms with commas, e.g. 0 => 0, 1 => 1, _ => 0.",
},
"Missing comma before MATCH default arm",
),
})
}
func TestMatchMissingCommaDiagnosticSpan(t *testing.T) {
c := compiler.New()
query := `FUNC fib(n) (
RETURN MATCH n (
0 => 0
1 => 1
_ => fib(n - 1) + fib(n - 2)
)
)
RETURN fib(10)`
_, err := c.Compile(source.NewAnonymous(query))
if err == nil {
t.Fatal("expected compilation error")
}
diag := firstCompilationError(err)
if diag == nil {
t.Fatalf("expected diagnostic, got %T", err)
}
if diag.Message != "Expected ',' between MATCH arms" {
t.Fatalf("unexpected diagnostic message: %q", diag.Message)
}
if len(diag.Spans) == 0 {
t.Fatal("expected diagnostic span")
}
if diag.Spans[0].Label != "missing comma" {
t.Fatalf("unexpected span label: %q", diag.Spans[0].Label)
}
line, col := diag.Source.LocationAt(diag.Spans[0].Span)
if line != 3 || col != 15 {
t.Fatalf("unexpected span location: got %d:%d, want 3:15", line, col)
}
formatted := pkgdiagnostics.Format(err)
if !strings.Contains(formatted, "3 | 0 => 0\n | ^ missing comma\n4 | 1 => 1") {
t.Fatalf("diagnostic should point after previous MATCH arm value, got:\n%s", formatted)
}
}
@@ -1,9 +1,13 @@
package compiler_test
import (
"strings"
"testing"
"github.com/MontFerret/ferret/v2/pkg/compiler"
pkgdiagnostics "github.com/MontFerret/ferret/v2/pkg/diagnostics"
parserd "github.com/MontFerret/ferret/v2/pkg/parser/diagnostics"
"github.com/MontFerret/ferret/v2/pkg/source"
"github.com/MontFerret/ferret/v2/test/spec"
. "github.com/MontFerret/ferret/v2/test/spec/compile"
)
@@ -295,3 +299,126 @@ func TestSyntaxErrors(t *testing.T) {
}, "Incomplete range 2"),
})
}
func TestMixedFunctionBodySyntaxDiagnosticDoesNotCascade(t *testing.T) {
query := `
FUNC fib(n) => (
RETURN MATCH n (
0 => 0,
1 => 1,
_ => fib(n - 1) + fib(n - 2)
)
)
RETURN fib(10)`
_, err := compiler.New().Compile(source.NewAnonymous(query))
if err == nil {
t.Fatal("expected compilation error")
}
diag := firstCompilationError(err)
if diag == nil {
t.Fatalf("expected diagnostic, got %T", err)
}
if diag.Kind != parserd.SyntaxError {
t.Fatalf("unexpected diagnostic kind: %s", diag.Kind)
}
if diag.Message != "Cannot combine arrow and block function body syntax" {
t.Fatalf("unexpected diagnostic message: %q", diag.Message)
}
if diag.Hint != "Use either 'FUNC f(x) => expr' or 'FUNC f(x) ( ... RETURN expr )'." {
t.Fatalf("unexpected diagnostic hint: %q", diag.Hint)
}
if diag.Note != "Remove '=>' to use a block body, or remove RETURN and keep a single expression after '=>'." {
t.Fatalf("unexpected diagnostic note: %q", diag.Note)
}
if len(diag.Spans) == 0 {
t.Fatal("expected diagnostic span")
}
if diag.Spans[0].Label != "RETURN is only valid in a block function body" {
t.Fatalf("unexpected span label: %q", diag.Spans[0].Label)
}
formatted := pkgdiagnostics.Format(err)
for _, unexpected := range []string{
"Unclosed parenthesized expression",
"mismatched input ')' expecting <EOF>",
"Variable 'n' is not defined",
} {
if strings.Contains(formatted, unexpected) {
t.Fatalf("formatted diagnostic contains cascade %q:\n%s", unexpected, formatted)
}
}
}
func TestMissingFunctionParamsCloseDiagnosticDoesNotCascade(t *testing.T) {
query := `FUNC fib (
RETURN MATCH n (
0 => 0,
1 => 1,
_ => fib(n - 1) + fib(n - 2)
)
)
RETURN fib(10)`
_, err := compiler.New().Compile(source.NewAnonymous(query))
if err == nil {
t.Fatal("expected compilation error")
}
diag := firstCompilationError(err)
if diag == nil {
t.Fatalf("expected diagnostic, got %T", err)
}
if diag.Kind != parserd.SyntaxError {
t.Fatalf("unexpected diagnostic kind: %s", diag.Kind)
}
if diag.Message != "Expected function parameters before function body" {
t.Fatalf("unexpected diagnostic message: %q", diag.Message)
}
if diag.Hint != "Add a parameter list before the block body, e.g. FUNC fib(n) ( ... RETURN expr ). Use FUNC fib() ( ... ) for no parameters." {
t.Fatalf("unexpected diagnostic hint: %q", diag.Hint)
}
if len(diag.Spans) == 0 {
t.Fatal("expected diagnostic span")
}
if diag.Spans[0].Label != "missing parameter list before function body" {
t.Fatalf("unexpected span label: %q", diag.Spans[0].Label)
}
line, col := diag.Source.LocationAt(diag.Spans[0].Span)
if line != 1 || col != 10 {
t.Fatalf("unexpected span location: got %d:%d, want 1:10", line, col)
}
formatted := pkgdiagnostics.Format(err)
if got := strings.Count(formatted, "SyntaxError:"); got != 1 {
t.Fatalf("expected one syntax diagnostic, got %d:\n%s", got, formatted)
}
if !strings.Contains(formatted, "1 | FUNC fib (\n | ^ missing parameter list before function body\n2 | RETURN MATCH n (") {
t.Fatalf("diagnostic should point at the premature function body paren, got:\n%s", formatted)
}
for _, unexpected := range []string{
"mismatched input 'RETURN'",
"mismatched input ')' expecting <EOF>",
} {
if strings.Contains(formatted, unexpected) {
t.Fatalf("formatted diagnostic contains cascade %q:\n%s", unexpected, formatted)
}
}
}
+4
View File
@@ -14,6 +14,10 @@ FUNC id(x) => x
RETURN id(1)
`, 1, "UDF arrow body"),
S(`
FUNC grouped() => (1 + 2)
RETURN grouped()
`, 3, "UDF arrow grouped expression"),
S(`
FUNC id(x) (
RETURN x
)