1
0
mirror of https://github.com/MontFerret/ferret.git synced 2025-08-13 19:52:52 +02:00
This commit is contained in:
Tim Voronov
2025-08-06 13:16:26 -04:00
parent 57390fc901
commit f520651b7a
25 changed files with 423 additions and 293 deletions

View File

@@ -1,14 +1,13 @@
package compiler
import (
"github.com/MontFerret/ferret/pkg/compiler/internal/diagnostics"
goruntime "runtime"
"github.com/antlr4-go/antlr/v4"
"github.com/MontFerret/ferret/pkg/file"
"github.com/MontFerret/ferret/pkg/compiler/internal/core"
"github.com/MontFerret/ferret/pkg/vm"
"github.com/MontFerret/ferret/pkg/parser"
@@ -29,10 +28,10 @@ func New(setters ...Option) *Compiler {
func (c *Compiler) Compile(src *file.Source) (program *vm.Program, err error) {
if src.Empty() {
return nil, core.NewEmptyQueryErr(src)
return nil, diagnostics.NewEmptyQueryErr(src)
}
errorHandler := core.NewErrorHandler(src, 10)
errorHandler := diagnostics.NewErrorHandler(src, 10)
defer func() {
if r := recover(); r != nil {
@@ -45,11 +44,11 @@ func (c *Compiler) Compile(src *file.Source) (program *vm.Program, err error) {
// Find out exactly what the error was and add the e
switch x := r.(type) {
case string:
e = core.NewInternalErr(src, x+"\n"+stackTrace)
e = diagnostics.NewInternalErr(src, x+"\n"+stackTrace)
case error:
e = core.NewInternalErrWith(src, "unknown panic\n"+stackTrace, x)
e = diagnostics.NewInternalErrWith(src, "unknown panic\n"+stackTrace, x)
default:
e = core.NewInternalErr(src, "unknown panic\n"+stackTrace)
e = diagnostics.NewInternalErr(src, "unknown panic\n"+stackTrace)
}
errorHandler.Add(e)
@@ -60,11 +59,11 @@ func (c *Compiler) Compile(src *file.Source) (program *vm.Program, err error) {
}()
l := NewVisitor(src, errorHandler)
tokenHistory := parser.NewTokenHistory(10)
tokenHistory := diagnostics.NewTokenHistory(10)
p := parser.New(src.Content(), func(stream antlr.TokenStream) antlr.TokenStream {
return parser.NewTrackingTokenStream(stream, tokenHistory)
return diagnostics.NewTrackingTokenStream(stream, tokenHistory)
})
p.AddErrorListener(newErrorListener(src, l.Ctx.Errors, tokenHistory))
p.AddErrorListener(diagnostics.NewErrorListener(src, l.Ctx.Errors, tokenHistory))
p.Visit(l)
if l.Ctx.Errors.HasErrors() {

View File

@@ -1,17 +1,19 @@
package compiler
import "github.com/MontFerret/ferret/pkg/compiler/internal/core"
import (
"github.com/MontFerret/ferret/pkg/compiler/internal/diagnostics"
)
type ErrorKind = core.ErrorKind
type CompilationError = core.CompilationError
type MultiCompilationError = core.MultiCompilationError
type ErrorKind = diagnostics.ErrorKind
type CompilationError = diagnostics.CompilationError
type MultiCompilationError = diagnostics.MultiCompilationError
var (
UnknownError = core.UnknownError
SyntaxError = core.SyntaxError
NameError = core.NameError
TypeError = core.TypeError
SemanticError = core.SemanticError
UnsupportedError = core.UnsupportedError
InternalError = core.InternalError
UnknownError = diagnostics.UnknownError
SyntaxError = diagnostics.SyntaxError
NameError = diagnostics.NameError
TypeError = diagnostics.TypeError
SemanticError = diagnostics.SemanticError
UnsupportedError = diagnostics.UnsupportedError
InternalError = diagnostics.InternalError
)

View File

@@ -1,173 +0,0 @@
package compiler
import (
"fmt"
"strings"
"github.com/MontFerret/ferret/pkg/parser/fql"
"github.com/antlr4-go/antlr/v4"
"github.com/MontFerret/ferret/pkg/file"
"github.com/MontFerret/ferret/pkg/parser"
"github.com/MontFerret/ferret/pkg/compiler/internal/core"
)
type (
errorListener struct {
*antlr.DiagnosticErrorListener
src *file.Source
handler *core.ErrorHandler
history *parser.TokenHistory
}
errorPattern struct {
Name string
MatchFn func(tokens []antlr.Token) (matched bool, info map[string]string)
Explain func(info map[string]string) (msg, hint string, span file.Span)
}
)
func newErrorListener(src *file.Source, handler *core.ErrorHandler, history *parser.TokenHistory) antlr.ErrorListener {
return &errorListener{
DiagnosticErrorListener: antlr.NewDiagnosticErrorListener(false),
src: src,
handler: handler,
history: history,
}
}
func (d *errorListener) ReportAttemptingFullContext(recognizer antlr.Parser, dfa *antlr.DFA, startIndex, stopIndex int, conflictingAlts *antlr.BitSet, configs *antlr.ATNConfigSet) {
}
func (d *errorListener) ReportContextSensitivity(recognizer antlr.Parser, dfa *antlr.DFA, startIndex, stopIndex, prediction int, configs *antlr.ATNConfigSet) {
}
func (d *errorListener) SyntaxError(_ antlr.Recognizer, offendingSymbol interface{}, line, column int, msg string, e antlr.RecognitionException) {
var offending antlr.Token
// Get offending token
if tok, ok := offendingSymbol.(antlr.Token); ok {
offending = tok
}
d.handler.Add(d.parseError(msg, offending))
}
func (d *errorListener) parseError(msg string, offending antlr.Token) *CompilationError {
span := core.SpanFromTokenSafe(offending, d.src)
err := &CompilationError{
Kind: SyntaxError,
Message: "Syntax error: " + msg,
Hint: "Check your syntax. Did you forget to write something?",
Spans: []core.ErrorSpan{
{Span: span, Main: true},
},
}
for _, handler := range []func(*CompilationError) bool{
d.extraneousError,
d.noViableAltError,
} {
if handler(err) {
break
}
}
return err
}
func (d *errorListener) extraneousError(err *CompilationError) (matched bool) {
if !strings.Contains(err.Message, "extraneous input") {
return false
}
last := d.history.Last()
if last == nil {
return false
}
span := core.SpanFromTokenSafe(last.Token(), d.src)
err.Spans = []core.ErrorSpan{
core.NewMainErrorSpan(span, "query must end with a value"),
}
err.Message = "Expected a RETURN or FOR clause at end of query"
err.Hint = "All queries must return a value. Add a RETURN statement to complete the query."
return true
}
func (d *errorListener) noViableAltError(err *CompilationError) bool {
if !strings.Contains(err.Message, "viable alternative at input") {
return false
}
if d.history.Size() < 2 {
return false
}
// most recent (offending)
last := d.history.Last()
// CASE: RETURN [missing value]
if isToken(last, "RETURN") && isKeyword(last.Token()) {
span := core.SpanFromTokenSafe(last.Token(), d.src)
err.Message = fmt.Sprintf("Expected expression after '%s'", last)
err.Hint = "Did you forget to provide a value to return?"
err.Spans = []core.ErrorSpan{
core.NewMainErrorSpan(span, "missing return value"),
}
return true
}
// CASE: LET x = [missing value]
//if strtoken(last.Token()) == "LET" && isIdentifier(tokens[n-2]) && t1.GetText() == "=" {
// varName := tokens[n-2].GetText()
// span := core.SpanFromTokenSafe(tokens[n-1], d.src)
//
// err.Message = fmt.Sprintf("Expected expression after '=' for variable '%s'", varName)
// err.Hint = "Did you forget to provide a value?"
// err.Spans = []core.ErrorSpan{
// core.NewMainErrorSpan(span, "missing value"),
// }
// return true
//}
return false
}
func isIdentifier(token antlr.Token) bool {
if token == nil {
return false
}
tt := token.GetTokenType()
return tt == fql.FqlLexerIdentifier || tt == fql.FqlLexerIgnoreIdentifier
}
func isKeyword(token antlr.Token) bool {
if token == nil {
return false
}
ttype := token.GetTokenType()
// 0 is usually invalid; <EOF> is -1
if ttype <= 0 || ttype >= len(fql.FqlLexerLexerStaticData.LiteralNames) {
return false
}
lit := fql.FqlLexerLexerStaticData.LiteralNames[ttype]
return strings.HasPrefix(lit, "'") && strings.HasSuffix(lit, "'")
}
func isToken(node *parser.TokenNode, expected string) bool {
return strings.ToUpper(node.String()) == expected
}

View File

@@ -2,6 +2,7 @@ package internal
import (
"github.com/MontFerret/ferret/pkg/compiler/internal/core"
"github.com/MontFerret/ferret/pkg/compiler/internal/diagnostics"
"github.com/MontFerret/ferret/pkg/file"
)
@@ -13,7 +14,7 @@ type CompilerContext struct {
Symbols *core.SymbolTable
Loops *core.LoopTable
CatchTable *core.CatchStack
Errors *core.ErrorHandler
Errors *diagnostics.ErrorHandler
ExprCompiler *ExprCompiler
LiteralCompiler *LiteralCompiler
@@ -25,7 +26,7 @@ type CompilerContext struct {
}
// NewCompilerContext initializes a new CompilerContext with default values.
func NewCompilerContext(src *file.Source, errors *core.ErrorHandler) *CompilerContext {
func NewCompilerContext(src *file.Source, errors *diagnostics.ErrorHandler) *CompilerContext {
ctx := &CompilerContext{
Source: src,
Errors: errors,

View File

@@ -1,55 +0,0 @@
package core
import (
"github.com/antlr4-go/antlr/v4"
"github.com/MontFerret/ferret/pkg/file"
)
func SpanFromRuleContext(ctx antlr.ParserRuleContext) file.Span {
start := ctx.GetStart()
stop := ctx.GetStop()
if start == nil || stop == nil {
return file.Span{Start: 0, End: 0}
}
return file.Span{Start: start.GetStart(), End: stop.GetStop() + 1}
}
func SpanFromToken(tok antlr.Token) file.Span {
if tok == nil {
return file.Span{Start: 0, End: 0}
}
return file.Span{Start: tok.GetStart(), End: tok.GetStop() + 1}
}
func SpanFromTokenSafe(tok antlr.Token, src *file.Source) file.Span {
if tok == nil {
return file.Span{Start: 0, End: 1}
}
start := tok.GetStart()
end := tok.GetStop() + 1 // exclusive end
if start < 0 {
start = 0
}
if end <= start {
end = start + 1
}
// clamp to source length
maxLen := len(src.Content())
if end > maxLen {
end = maxLen
}
if start > maxLen {
start = maxLen - 1
}
return file.Span{Start: start, End: end}
}

View File

@@ -1,4 +1,4 @@
package core
package diagnostics
import (
"strings"

View File

@@ -0,0 +1,101 @@
package diagnostics
import (
"strings"
"github.com/antlr4-go/antlr/v4"
"github.com/MontFerret/ferret/pkg/file"
)
type ErrorListener struct {
*antlr.DiagnosticErrorListener
src *file.Source
handler *ErrorHandler
history *TokenHistory
}
func NewErrorListener(src *file.Source, handler *ErrorHandler, history *TokenHistory) antlr.ErrorListener {
return &ErrorListener{
DiagnosticErrorListener: antlr.NewDiagnosticErrorListener(false),
src: src,
handler: handler,
history: history,
}
}
func (d *ErrorListener) ReportAttemptingFullContext(recognizer antlr.Parser, dfa *antlr.DFA, startIndex, stopIndex int, conflictingAlts *antlr.BitSet, configs *antlr.ATNConfigSet) {
}
func (d *ErrorListener) ReportContextSensitivity(recognizer antlr.Parser, dfa *antlr.DFA, startIndex, stopIndex, prediction int, configs *antlr.ATNConfigSet) {
}
func (d *ErrorListener) SyntaxError(_ antlr.Recognizer, offendingSymbol interface{}, line, column int, msg string, e antlr.RecognitionException) {
var offending antlr.Token
// Get offending token
if tok, ok := offendingSymbol.(antlr.Token); ok {
offending = tok
}
d.handler.Add(d.parseError(msg, offending))
}
func (d *ErrorListener) parseError(msg string, offending antlr.Token) *CompilationError {
span := spanFromTokenSafe(offending, d.src)
err := &CompilationError{
Source: d.src,
Kind: SyntaxError,
Message: "Syntax error: " + msg,
Hint: "Check your syntax. Did you forget to write something?",
Spans: []ErrorSpan{
{Span: span, Main: true},
},
}
for _, handler := range []func(*CompilationError) bool{
d.extraneousError,
d.noViableAltError,
} {
if handler(err) {
break
}
}
return err
}
func (d *ErrorListener) extraneousError(err *CompilationError) (matched bool) {
if !strings.Contains(err.Message, "extraneous input") {
return false
}
last := d.history.Last()
if last == nil {
return false
}
span := spanFromTokenSafe(last.Token(), d.src)
err.Spans = []ErrorSpan{
NewMainErrorSpan(span, "query must end with a value"),
}
err.Message = "Expected a RETURN or FOR clause at end of query"
err.Hint = "All queries must return a value. Add a RETURN statement to complete the query."
return true
}
func (d *ErrorListener) noViableAltError(err *CompilationError) bool {
if !strings.Contains(err.Message, "viable alternative at input") {
return false
}
if d.history.Size() < 2 {
return false
}
return AnalyzeSyntaxError(d.src, err, d.history.Last())
}

View File

@@ -1,4 +1,4 @@
package core
package diagnostics
import (
"fmt"

View File

@@ -1,4 +1,4 @@
package core
package diagnostics
import "github.com/MontFerret/ferret/pkg/file"

View File

@@ -1,4 +1,4 @@
package core
package diagnostics
import (
"github.com/MontFerret/ferret/pkg/file"

View File

@@ -1,4 +1,4 @@
package core
package diagnostics
import (
"fmt"

View File

@@ -1,4 +1,4 @@
package core
package diagnostics
import (
"fmt"

View File

@@ -0,0 +1,116 @@
package diagnostics
import (
"github.com/MontFerret/ferret/pkg/parser/fql"
"github.com/antlr4-go/antlr/v4"
"strings"
"github.com/MontFerret/ferret/pkg/file"
)
func SpanFromRuleContext(ctx antlr.ParserRuleContext) file.Span {
start := ctx.GetStart()
stop := ctx.GetStop()
if start == nil || stop == nil {
return file.Span{Start: 0, End: 0}
}
return file.Span{Start: start.GetStart(), End: stop.GetStop() + 1}
}
func SpanFromToken(tok antlr.Token) file.Span {
if tok == nil {
return file.Span{Start: 0, End: 0}
}
return file.Span{Start: tok.GetStart(), End: tok.GetStop() + 1}
}
func spanFromTokenSafe(tok antlr.Token, src *file.Source) file.Span {
if tok == nil {
return file.Span{Start: 0, End: 1}
}
start := tok.GetStart()
end := tok.GetStop() + 1 // exclusive end
if start < 0 {
start = 0
}
if end <= start {
end = start + 1
}
// clamp to source length
maxLen := len(src.Content())
if end > maxLen {
end = maxLen
}
if start > maxLen {
start = maxLen - 1
}
return file.Span{Start: start, End: end}
}
func stringify(token *TokenNode) string {
if token == nil {
return ""
}
return strings.ToUpper(strings.TrimSpace(token.GetText()))
}
func isIdentifier(node *TokenNode) bool {
if node == nil {
return false
}
token := node.Token()
if token == nil {
return false
}
tt := token.GetTokenType()
return tt == fql.FqlLexerIdentifier || tt == fql.FqlLexerIgnoreIdentifier
}
func isKeyword(node *TokenNode) bool {
if node == nil {
return false
}
token := node.Token()
if token == nil {
return false
}
ttype := token.GetTokenType()
// 0 is usually invalid; <EOF> is -1
if ttype <= 0 || ttype >= len(fql.FqlLexerLexerStaticData.LiteralNames) {
return false
}
lit := fql.FqlLexerLexerStaticData.LiteralNames[ttype]
return strings.HasPrefix(lit, "'") && strings.HasSuffix(lit, "'")
}
func is(node *TokenNode, expected string) bool {
if node == nil {
return false
}
if node.GetText() == "" {
return false
}
return strings.ToUpper(node.GetText()) == expected
}

View File

@@ -0,0 +1,26 @@
package diagnostics
import (
"fmt"
"github.com/MontFerret/ferret/pkg/file"
)
func missingAssignmentValueMatcher(src *file.Source, err *CompilationError, offending *TokenNode) bool {
prev := offending.Prev()
// CASE: LET x = [missing value]
if is(offending, "LET") || is(prev, "=") {
span := spanFromTokenSafe(prev.Token(), src)
span.Start++
span.End++
err.Message = fmt.Sprintf("Expected expression after '=' for variable '%s'", prev.Prev())
err.Hint = "Did you forget to provide a value?"
err.Spans = []ErrorSpan{
NewMainErrorSpan(span, "missing value"),
}
return true
}
return false
}

View File

@@ -0,0 +1,27 @@
package diagnostics
import (
"fmt"
"github.com/MontFerret/ferret/pkg/file"
)
func missingReturnValueMatcher(src *file.Source, err *CompilationError, offending *TokenNode) bool {
if !is(offending, "RETURN") {
return false
}
prev := offending.Prev()
if prev != nil && isKeyword(prev) && !is(prev, "NONE") && !is(prev, "NULL") {
return false
}
span := spanFromTokenSafe(offending.Token(), src)
err.Message = fmt.Sprintf("Expected expression after '%s'", offending)
err.Hint = "Did you forget to provide a value to return?"
err.Spans = []ErrorSpan{
NewMainErrorSpan(span, "missing return value"),
}
return true
}

View File

@@ -0,0 +1,22 @@
package diagnostics
import (
"github.com/MontFerret/ferret/pkg/file"
)
type SyntaxErrorMatcher func(src *file.Source, err *CompilationError, offending *TokenNode) bool
func AnalyzeSyntaxError(src *file.Source, err *CompilationError, offending *TokenNode) bool {
matchers := []SyntaxErrorMatcher{
missingAssignmentValueMatcher,
missingReturnValueMatcher,
}
for _, matcher := range matchers {
if matcher(src, err, offending) {
return true
}
}
return false
}

View File

@@ -1,4 +1,4 @@
package parser
package diagnostics
import "github.com/antlr4-go/antlr/v4"
@@ -25,6 +25,7 @@ func (h *TokenHistory) Add(token antlr.Token) {
// Avoid adding the same token twice in a row (by position, not just text)
if h.head != nil {
last := h.head.token
if last.GetStart() == token.GetStart() &&
last.GetStop() == token.GetStop() &&
last.GetTokenType() == token.GetTokenType() {
@@ -35,8 +36,8 @@ func (h *TokenHistory) Add(token antlr.Token) {
node := &TokenNode{token: token}
if h.head != nil {
node.next = h.head
h.head.prev = node
node.prev = h.head
h.head.next = node
}
h.head = node
@@ -49,7 +50,7 @@ func (h *TokenHistory) Add(token antlr.Token) {
if h.size > h.cap {
// Remove oldest
h.tail = h.tail.prev
h.tail = h.tail.next
if h.tail != nil {
h.tail.next = nil

View File

@@ -1,4 +1,4 @@
package parser
package diagnostics
import (
"github.com/antlr4-go/antlr/v4"
@@ -50,6 +50,14 @@ func (t *TokenNode) NextAt(n int) *TokenNode {
return node
}
func (t *TokenNode) String() string {
func (t *TokenNode) GetText() string {
if t.token == nil {
return ""
}
return t.token.GetText()
}
func (t *TokenNode) String() string {
return t.GetText()
}

View File

@@ -1,4 +1,4 @@
package parser
package diagnostics
import "github.com/antlr4-go/antlr/v4"

View File

@@ -397,7 +397,7 @@ func (c *ExprCompiler) compileAtom(ctx fql.IExpressionAtomContext) vm.Operand {
return c.Compile(e)
}
c.ctx.Errors.UnexpectedToken(ctx)
//c.ctx.Errors.UnexpectedToken(ctx)
return vm.NoopOperand
}

View File

@@ -37,6 +37,11 @@ func (c *LoopCompiler) compileForIn(ctx fql.IForExpressionContext) vm.Operand {
// Initialize the loop with ForInLoop type
returnRuleCtx := c.compileInitialization(ctx, core.ForInLoop)
// Probably, a syntax error happened and no return rule context was created.
if returnRuleCtx == nil {
return vm.NoopOperand
}
// Compile the loop body (statements and clauses)
if body := ctx.AllForExpressionBody(); body != nil && len(body) > 0 {
for _, b := range body {
@@ -59,6 +64,11 @@ func (c *LoopCompiler) compileForWhile(ctx fql.IForExpressionContext) vm.Operand
// Initialize the loop with ForWhileLoop type
returnRuleCtx := c.compileInitialization(ctx, core.ForWhileLoop)
// Probably, a syntax error happened and no return rule context was created.
if returnRuleCtx == nil {
return vm.NoopOperand
}
// Compile the loop body (statements and clauses)
if body := ctx.AllForExpressionBody(); body != nil && len(body) > 0 {
for _, b := range body {
@@ -87,6 +97,10 @@ func (c *LoopCompiler) compileInitialization(ctx fql.IForExpressionContext, kind
var loopType core.LoopType
returnCtx := ctx.ForExpressionReturn()
if returnCtx == nil {
return nil
}
// Determine the loop type and whether it should use distinct values
if re := returnCtx.ReturnExpression(); re != nil {
returnRuleCtx = re

View File

@@ -2,7 +2,7 @@ package compiler
import (
"github.com/MontFerret/ferret/pkg/compiler/internal"
"github.com/MontFerret/ferret/pkg/compiler/internal/core"
"github.com/MontFerret/ferret/pkg/compiler/internal/diagnostics"
"github.com/MontFerret/ferret/pkg/file"
"github.com/MontFerret/ferret/pkg/parser/fql"
)
@@ -12,7 +12,7 @@ type Visitor struct {
Ctx *internal.CompilerContext
}
func NewVisitor(src *file.Source, errors *core.ErrorHandler) *Visitor {
func NewVisitor(src *file.Source, errors *diagnostics.ErrorHandler) *Visitor {
v := new(Visitor)
v.BaseFqlParserVisitor = new(fql.BaseFqlParserVisitor)
v.Ctx = internal.NewCompilerContext(src, errors)

View File

@@ -2,6 +2,7 @@ package base
import (
"fmt"
"github.com/smarty/assertions"
"github.com/MontFerret/ferret/pkg/compiler"
@@ -13,6 +14,7 @@ type (
Message string
Kind compiler.ErrorKind
Hint string
Format string
}
ExpectedMultiError struct {
@@ -62,6 +64,15 @@ func assertExpectedError(actual *compiler.CompilationError, expected *ExpectedEr
return fmt.Sprintf("expected error hint '%s', got '%s'", expected.Hint, actual.Hint)
}
if expected.Format != "" {
actualFormat := actual.Format()
equalityRes := assertions.ShouldEqual(actualFormat, expected.Format)
if equalityRes != "" {
return equalityRes
}
}
return ""
}

View File

@@ -0,0 +1,56 @@
package compiler_test
import (
"testing"
"github.com/MontFerret/ferret/pkg/compiler"
)
func TestSyntaxErrors(t *testing.T) {
RunUseCases(t, []UseCase{
ErrorCase(
`
LET i = NONE
`, E{
Kind: compiler.SyntaxError,
Message: "Expected a RETURN or FOR clause at end of query",
Hint: "All queries must return a value. Add a RETURN statement to complete the query.",
}, "Missing return statement"),
ErrorCase(
`
LET i = NONE
RETURN
`, E{
Kind: compiler.SyntaxError,
Message: "Expected expression after 'RETURN'",
Hint: "Did you forget to provide a value to return?",
}, "Missing return value"),
ErrorCase(
`
FOR i IN [1, 2, 3]
RETURN
`, E{
Kind: compiler.SyntaxError,
Message: "Expected expression after 'RETURN'",
Hint: "Did you forget to provide a value to return?",
}, "Missing return value in for loop"),
ErrorCase(
`
LET i =
RETURN i
`, E{
Kind: compiler.SyntaxError,
Message: "Expected expression after '=' for variable 'i'",
Hint: "Did you forget to provide a value?",
}, "Missing variable assignment value"),
ErrorCase(
`
FOR i IN
RETURN i
`, E{
Kind: compiler.SyntaxError,
Message: "__FAIL__",
Hint: "Did you forget to provide a value?",
}, "Missing iterable in FOR"),
})
}

View File

@@ -8,32 +8,6 @@ import (
func TestErrors(t *testing.T) {
RunUseCases(t, []UseCase{
ErrorCase(
`
LET i = NONE
`, E{
Kind: compiler.SyntaxError,
Message: "Expected a RETURN or FOR clause at end of query",
Hint: "All queries must return a value. Add a RETURN statement to complete the query.",
}, "Syntax error: missing return statement"),
ErrorCase(
`
LET i = NONE
RETURN
`, E{
Kind: compiler.SyntaxError,
Message: "Expected expression after 'RETURN'",
Hint: "Did you forget to provide a value to return?",
}, "Syntax error: missing return value"),
ErrorCase(
`
LET i =
RETURN i
`, E{
Kind: compiler.SyntaxError,
Message: "_FAIL_",
Hint: "",
}, "Syntax error: missing variable assignment value"),
ErrorCase(
`
LET i = NONE