mirror of
https://github.com/MontFerret/ferret.git
synced 2025-08-13 19:52:52 +02:00
wip
This commit is contained in:
@@ -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() {
|
||||
|
@@ -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
|
||||
)
|
||||
|
@@ -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
|
||||
}
|
@@ -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,
|
||||
|
@@ -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}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package core
|
||||
package diagnostics
|
||||
|
||||
import (
|
||||
"strings"
|
101
pkg/compiler/internal/diagnostics/error_listener.go
Normal file
101
pkg/compiler/internal/diagnostics/error_listener.go
Normal 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())
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package core
|
||||
package diagnostics
|
||||
|
||||
import (
|
||||
"fmt"
|
@@ -1,4 +1,4 @@
|
||||
package core
|
||||
package diagnostics
|
||||
|
||||
import "github.com/MontFerret/ferret/pkg/file"
|
||||
|
@@ -1,4 +1,4 @@
|
||||
package core
|
||||
package diagnostics
|
||||
|
||||
import (
|
||||
"github.com/MontFerret/ferret/pkg/file"
|
@@ -1,4 +1,4 @@
|
||||
package core
|
||||
package diagnostics
|
||||
|
||||
import (
|
||||
"fmt"
|
@@ -1,4 +1,4 @@
|
||||
package core
|
||||
package diagnostics
|
||||
|
||||
import (
|
||||
"fmt"
|
116
pkg/compiler/internal/diagnostics/helpers.go
Normal file
116
pkg/compiler/internal/diagnostics/helpers.go
Normal 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
|
||||
}
|
@@ -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
|
||||
}
|
@@ -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
|
||||
}
|
22
pkg/compiler/internal/diagnostics/syntax_error_matcher.go
Normal file
22
pkg/compiler/internal/diagnostics/syntax_error_matcher.go
Normal 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
|
||||
}
|
@@ -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
|
@@ -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()
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package parser
|
||||
package diagnostics
|
||||
|
||||
import "github.com/antlr4-go/antlr/v4"
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
@@ -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 ""
|
||||
}
|
||||
|
||||
|
56
test/integration/compiler/compiler_errors_syntax_test.go
Normal file
56
test/integration/compiler/compiler_errors_syntax_test.go
Normal 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"),
|
||||
})
|
||||
}
|
@@ -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
|
||||
|
Reference in New Issue
Block a user