diff --git a/pkg/compiler/compiler.go b/pkg/compiler/compiler.go index 9991614f..afeb2efe 100644 --- a/pkg/compiler/compiler.go +++ b/pkg/compiler/compiler.go @@ -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() { diff --git a/pkg/compiler/error.go b/pkg/compiler/error.go index 4153ad26..287cb136 100644 --- a/pkg/compiler/error.go +++ b/pkg/compiler/error.go @@ -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 ) diff --git a/pkg/compiler/error_listener.go b/pkg/compiler/error_listener.go deleted file mode 100644 index b428b862..00000000 --- a/pkg/compiler/error_listener.go +++ /dev/null @@ -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; 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 -} diff --git a/pkg/compiler/internal/context.go b/pkg/compiler/internal/context.go index 99a535d6..0e1615a6 100644 --- a/pkg/compiler/internal/context.go +++ b/pkg/compiler/internal/context.go @@ -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, diff --git a/pkg/compiler/internal/core/error_helpers.go b/pkg/compiler/internal/core/error_helpers.go deleted file mode 100644 index 6c3bf939..00000000 --- a/pkg/compiler/internal/core/error_helpers.go +++ /dev/null @@ -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} -} diff --git a/pkg/compiler/internal/core/error.go b/pkg/compiler/internal/diagnostics/error.go similarity index 97% rename from pkg/compiler/internal/core/error.go rename to pkg/compiler/internal/diagnostics/error.go index 18d6bc36..14b9cc13 100644 --- a/pkg/compiler/internal/core/error.go +++ b/pkg/compiler/internal/diagnostics/error.go @@ -1,4 +1,4 @@ -package core +package diagnostics import ( "strings" diff --git a/pkg/compiler/internal/diagnostics/error_listener.go b/pkg/compiler/internal/diagnostics/error_listener.go new file mode 100644 index 00000000..68a1a42c --- /dev/null +++ b/pkg/compiler/internal/diagnostics/error_listener.go @@ -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()) +} diff --git a/pkg/compiler/internal/core/error_multi.go b/pkg/compiler/internal/diagnostics/error_multi.go similarity index 96% rename from pkg/compiler/internal/core/error_multi.go rename to pkg/compiler/internal/diagnostics/error_multi.go index bfa9458f..7146e041 100644 --- a/pkg/compiler/internal/core/error_multi.go +++ b/pkg/compiler/internal/diagnostics/error_multi.go @@ -1,4 +1,4 @@ -package core +package diagnostics import ( "fmt" diff --git a/pkg/compiler/internal/core/error_span.go b/pkg/compiler/internal/diagnostics/error_span.go similarity index 96% rename from pkg/compiler/internal/core/error_span.go rename to pkg/compiler/internal/diagnostics/error_span.go index 7318c4f4..c670a307 100644 --- a/pkg/compiler/internal/core/error_span.go +++ b/pkg/compiler/internal/diagnostics/error_span.go @@ -1,4 +1,4 @@ -package core +package diagnostics import "github.com/MontFerret/ferret/pkg/file" diff --git a/pkg/compiler/internal/core/errors.go b/pkg/compiler/internal/diagnostics/errors.go similarity index 97% rename from pkg/compiler/internal/core/errors.go rename to pkg/compiler/internal/diagnostics/errors.go index aa0bd3df..5d2675c7 100644 --- a/pkg/compiler/internal/core/errors.go +++ b/pkg/compiler/internal/diagnostics/errors.go @@ -1,4 +1,4 @@ -package core +package diagnostics import ( "github.com/MontFerret/ferret/pkg/file" diff --git a/pkg/compiler/internal/core/error_formatter.go b/pkg/compiler/internal/diagnostics/formatter.go similarity index 98% rename from pkg/compiler/internal/core/error_formatter.go rename to pkg/compiler/internal/diagnostics/formatter.go index d30c8f15..35a0cdbc 100644 --- a/pkg/compiler/internal/core/error_formatter.go +++ b/pkg/compiler/internal/diagnostics/formatter.go @@ -1,4 +1,4 @@ -package core +package diagnostics import ( "fmt" diff --git a/pkg/compiler/internal/core/error_handler.go b/pkg/compiler/internal/diagnostics/handler.go similarity index 99% rename from pkg/compiler/internal/core/error_handler.go rename to pkg/compiler/internal/diagnostics/handler.go index 1baac2b2..77514c66 100644 --- a/pkg/compiler/internal/core/error_handler.go +++ b/pkg/compiler/internal/diagnostics/handler.go @@ -1,4 +1,4 @@ -package core +package diagnostics import ( "fmt" diff --git a/pkg/compiler/internal/diagnostics/helpers.go b/pkg/compiler/internal/diagnostics/helpers.go new file mode 100644 index 00000000..27ccfc63 --- /dev/null +++ b/pkg/compiler/internal/diagnostics/helpers.go @@ -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; 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 +} diff --git a/pkg/compiler/internal/diagnostics/missing_assignment_value_matcher.go b/pkg/compiler/internal/diagnostics/missing_assignment_value_matcher.go new file mode 100644 index 00000000..728d3a16 --- /dev/null +++ b/pkg/compiler/internal/diagnostics/missing_assignment_value_matcher.go @@ -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 +} diff --git a/pkg/compiler/internal/diagnostics/missing_return_value_matcher.go.go b/pkg/compiler/internal/diagnostics/missing_return_value_matcher.go.go new file mode 100644 index 00000000..99a7e568 --- /dev/null +++ b/pkg/compiler/internal/diagnostics/missing_return_value_matcher.go.go @@ -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 +} diff --git a/pkg/compiler/internal/diagnostics/syntax_error_matcher.go b/pkg/compiler/internal/diagnostics/syntax_error_matcher.go new file mode 100644 index 00000000..5608b9cd --- /dev/null +++ b/pkg/compiler/internal/diagnostics/syntax_error_matcher.go @@ -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 +} diff --git a/pkg/parser/token_history.go b/pkg/compiler/internal/diagnostics/token_history.go similarity index 92% rename from pkg/parser/token_history.go rename to pkg/compiler/internal/diagnostics/token_history.go index d51525e4..bde52565 100644 --- a/pkg/parser/token_history.go +++ b/pkg/compiler/internal/diagnostics/token_history.go @@ -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 diff --git a/pkg/parser/token_node.go b/pkg/compiler/internal/diagnostics/token_node.go similarity index 85% rename from pkg/parser/token_node.go rename to pkg/compiler/internal/diagnostics/token_node.go index 30e0f786..004033b2 100644 --- a/pkg/parser/token_node.go +++ b/pkg/compiler/internal/diagnostics/token_node.go @@ -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() +} diff --git a/pkg/parser/stream_track_tokens.go b/pkg/compiler/internal/diagnostics/tracking_token_stream.go similarity index 95% rename from pkg/parser/stream_track_tokens.go rename to pkg/compiler/internal/diagnostics/tracking_token_stream.go index 84176cb7..02e02fcd 100644 --- a/pkg/parser/stream_track_tokens.go +++ b/pkg/compiler/internal/diagnostics/tracking_token_stream.go @@ -1,4 +1,4 @@ -package parser +package diagnostics import "github.com/antlr4-go/antlr/v4" diff --git a/pkg/compiler/internal/expr.go b/pkg/compiler/internal/expr.go index 3d2b0cad..720c88b0 100644 --- a/pkg/compiler/internal/expr.go +++ b/pkg/compiler/internal/expr.go @@ -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 } diff --git a/pkg/compiler/internal/loop.go b/pkg/compiler/internal/loop.go index e2ff9ad5..c4719cd3 100644 --- a/pkg/compiler/internal/loop.go +++ b/pkg/compiler/internal/loop.go @@ -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 diff --git a/pkg/compiler/visitor.go b/pkg/compiler/visitor.go index 9315435b..ca87c94f 100644 --- a/pkg/compiler/visitor.go +++ b/pkg/compiler/visitor.go @@ -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) diff --git a/test/integration/base/assertions.go b/test/integration/base/assertions.go index 0180323f..258b1c14 100644 --- a/test/integration/base/assertions.go +++ b/test/integration/base/assertions.go @@ -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 "" } diff --git a/test/integration/compiler/compiler_errors_syntax_test.go b/test/integration/compiler/compiler_errors_syntax_test.go new file mode 100644 index 00000000..226b58c2 --- /dev/null +++ b/test/integration/compiler/compiler_errors_syntax_test.go @@ -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"), + }) +} diff --git a/test/integration/compiler/compiler_errors_test.go b/test/integration/compiler/compiler_errors_test.go index 2ec1d091..c83e962a 100644 --- a/test/integration/compiler/compiler_errors_test.go +++ b/test/integration/compiler/compiler_errors_test.go @@ -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