mirror of
https://github.com/MontFerret/ferret.git
synced 2026-06-20 01:17:53 +02:00
Feat/parser error imp (#944)
* feat: add diagnostics for assignment in FILTER predicates * feat: enhance timeout handling in recovery compiler and diagnostics * Updated compiler error handling of using undeclared variables * feat: add diagnostics for filter assignment errors in predicates
This commit is contained in:
@@ -152,7 +152,7 @@ func (c *BindingCompiler) CompileAssignmentStatement(ctx fql.IAssignmentStatemen
|
||||
return bytecode.NoopOperand
|
||||
}
|
||||
|
||||
c.ctx.Program.Errors.VariableNotFound(assignment.RootTok, assignment.Root)
|
||||
reportVariableNotFound(c.ctx, assignment.RootTok, assignment.Root)
|
||||
return bytecode.NoopOperand
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ func (c *BindingCompiler) CompileDeleteStatement(ctx fql.IDeleteStatementContext
|
||||
return bytecode.NoopOperand
|
||||
}
|
||||
|
||||
c.ctx.Program.Errors.VariableNotFound(deletion.RootTok, deletion.Root)
|
||||
reportVariableNotFound(c.ctx, deletion.RootTok, deletion.Root)
|
||||
return bytecode.NoopOperand
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ type (
|
||||
HostFunctions *core.HostFunctionTable
|
||||
Errors *diagnostics.ErrorHandler
|
||||
Source *source.Source
|
||||
ForwardBindings *ForwardBindingIndex
|
||||
UseAliases map[string]string
|
||||
aggregatePlanByHash map[uint64][]int
|
||||
aggregatePlans []*bytecode.AggregatePlan
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package internal
|
||||
|
||||
import "github.com/antlr4-go/antlr/v4"
|
||||
|
||||
func reportVariableNotFound(ctx *CompilationSession, token antlr.Token, name string) {
|
||||
if ctx == nil || ctx.Program == nil || ctx.Program.Errors == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if index := ctx.Program.ForwardBindings; index != nil {
|
||||
if declaration, ok := index.Lookup(token, name); ok {
|
||||
ctx.Program.Errors.VariableUsedBeforeDeclaration(token, name, declaration)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Program.Errors.VariableNotFound(token, name)
|
||||
}
|
||||
@@ -77,7 +77,7 @@ func (c *exprCallCompiler) compileVariable(ctx fql.IVariableContext) bytecode.Op
|
||||
|
||||
binding, found := c.ctx.Function.Symbols.ResolveBinding(name)
|
||||
if !found {
|
||||
c.ctx.Program.Errors.VariableNotFound(token, name)
|
||||
reportVariableNotFound(c.ctx, token, name)
|
||||
|
||||
return bytecode.NoopOperand
|
||||
}
|
||||
|
||||
@@ -0,0 +1,450 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"github.com/antlr4-go/antlr/v4"
|
||||
|
||||
"github.com/MontFerret/ferret/v2/pkg/compiler/internal/core"
|
||||
parserd "github.com/MontFerret/ferret/v2/pkg/parser/diagnostics"
|
||||
"github.com/MontFerret/ferret/v2/pkg/parser/fql"
|
||||
"github.com/MontFerret/ferret/v2/pkg/source"
|
||||
)
|
||||
|
||||
type (
|
||||
// ForwardBindingIndex records source-level binding declarations before
|
||||
// lowering so unresolved uses can point at declarations that appear later.
|
||||
ForwardBindingIndex struct {
|
||||
declarations []forwardBindingDeclaration
|
||||
}
|
||||
|
||||
forwardBindingDeclaration struct {
|
||||
Ctx antlr.ParserRuleContext
|
||||
Name string
|
||||
Scope forwardBindingScope
|
||||
Span source.Span
|
||||
}
|
||||
|
||||
forwardBindingScope struct {
|
||||
Span source.Span
|
||||
Depth int
|
||||
}
|
||||
)
|
||||
|
||||
// NewForwardBindingIndex creates an empty declaration index for one compile.
|
||||
func NewForwardBindingIndex(_ *CompilationSession) *ForwardBindingIndex {
|
||||
return &ForwardBindingIndex{}
|
||||
}
|
||||
|
||||
// BuildProgram indexes declarations by lexical scope without changing compiler state.
|
||||
func (i *ForwardBindingIndex) BuildProgram(program *fql.ProgramContext) {
|
||||
if i == nil {
|
||||
return
|
||||
}
|
||||
|
||||
i.declarations = i.declarations[:0]
|
||||
|
||||
if program == nil || program.Body() == nil {
|
||||
return
|
||||
}
|
||||
|
||||
body, ok := program.Body().(*fql.BodyContext)
|
||||
if !ok || body == nil {
|
||||
return
|
||||
}
|
||||
|
||||
i.buildBody(body, i.scopeFromContext(body, 0))
|
||||
}
|
||||
|
||||
// Lookup finds the nearest later declaration visible from the unresolved token.
|
||||
func (i *ForwardBindingIndex) Lookup(token antlr.Token, name string) (antlr.ParserRuleContext, bool) {
|
||||
if i == nil || token == nil || name == "" || name == core.IgnorePseudoVariable || name == core.PseudoVariable {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
useStart := token.GetStart()
|
||||
var best *forwardBindingDeclaration
|
||||
|
||||
for idx := range i.declarations {
|
||||
decl := &i.declarations[idx]
|
||||
if decl.Name != name || decl.Ctx == nil || decl.Span.Start <= useStart || !i.scopeContains(decl.Scope, useStart) {
|
||||
continue
|
||||
}
|
||||
|
||||
if best == nil || decl.Scope.Depth > best.Scope.Depth || decl.Scope.Depth == best.Scope.Depth && decl.Span.Start < best.Span.Start {
|
||||
best = decl
|
||||
}
|
||||
}
|
||||
|
||||
if best == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return best.Ctx, true
|
||||
}
|
||||
|
||||
func (i *ForwardBindingIndex) buildBody(body *fql.BodyContext, scope forwardBindingScope) {
|
||||
if i == nil || body == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, stmt := range body.AllBodyStatement() {
|
||||
i.buildBodyStatement(stmt, scope)
|
||||
}
|
||||
|
||||
i.buildBodyExpression(body.BodyExpression(), scope)
|
||||
}
|
||||
|
||||
func (i *ForwardBindingIndex) buildBodyStatement(ctx fql.IBodyStatementContext, scope forwardBindingScope) {
|
||||
if i == nil || ctx == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case ctx.VariableDeclaration() != nil:
|
||||
i.buildVariableDeclaration(ctx.VariableDeclaration(), scope)
|
||||
case ctx.FunctionDeclaration() != nil:
|
||||
i.buildFunctionDeclaration(ctx.FunctionDeclaration(), scope)
|
||||
default:
|
||||
i.collectNestedScopes(ctx, scope)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *ForwardBindingIndex) buildBodyExpression(ctx fql.IBodyExpressionContext, scope forwardBindingScope) {
|
||||
if i == nil || ctx == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if fe := ctx.ForExpression(); fe != nil {
|
||||
i.buildForExpression(fe, scope)
|
||||
return
|
||||
}
|
||||
|
||||
if ret := ctx.ReturnExpression(); ret != nil {
|
||||
i.collectNestedScopes(ret.Expression(), scope)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *ForwardBindingIndex) buildFunctionDeclaration(ctx fql.IFunctionDeclarationContext, parent forwardBindingScope) {
|
||||
if i == nil || ctx == nil || ctx.FunctionBody() == nil {
|
||||
return
|
||||
}
|
||||
|
||||
body := ctx.FunctionBody()
|
||||
scope := i.scopeFromNode(body, parent.Depth+1)
|
||||
|
||||
if arrow := body.FunctionArrow(); arrow != nil {
|
||||
i.collectNestedScopes(arrow.Expression(), scope)
|
||||
return
|
||||
}
|
||||
|
||||
block := body.FunctionBlock()
|
||||
if block == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, stmt := range block.AllFunctionStatement() {
|
||||
i.buildFunctionStatement(stmt, scope)
|
||||
}
|
||||
|
||||
if ret := block.FunctionReturn(); ret != nil {
|
||||
i.collectNestedScopes(ret.Expression(), scope)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *ForwardBindingIndex) buildFunctionStatement(ctx fql.IFunctionStatementContext, scope forwardBindingScope) {
|
||||
if i == nil || ctx == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case ctx.VariableDeclaration() != nil:
|
||||
i.buildVariableDeclaration(ctx.VariableDeclaration(), scope)
|
||||
case ctx.FunctionDeclaration() != nil:
|
||||
i.buildFunctionDeclaration(ctx.FunctionDeclaration(), scope)
|
||||
default:
|
||||
i.collectNestedScopes(ctx, scope)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *ForwardBindingIndex) buildForExpression(ctx fql.IForExpressionContext, parent forwardBindingScope) {
|
||||
if i == nil || ctx == nil {
|
||||
return
|
||||
}
|
||||
|
||||
scope := i.loopScope(ctx, parent)
|
||||
|
||||
if src := ctx.ForExpressionSource(); src != nil {
|
||||
i.collectNestedScopes(src, parent)
|
||||
} else {
|
||||
i.collectNestedScopes(ctx.Expression(), scope)
|
||||
}
|
||||
|
||||
if val := ctx.GetValueVariable(); val != nil {
|
||||
i.recordDeclaration(textOfLoopVariable(val), i.ruleContext(val), scope)
|
||||
}
|
||||
|
||||
if counter := ctx.GetCounterVariable(); counter != nil {
|
||||
i.recordDeclaration(textOfBindingIdentifier(counter), i.ruleContext(counter), scope)
|
||||
}
|
||||
|
||||
for _, body := range ctx.AllForExpressionBody() {
|
||||
i.buildForExpressionBody(body, scope)
|
||||
}
|
||||
|
||||
i.buildForExpressionReturn(ctx.ForExpressionReturn(), scope)
|
||||
}
|
||||
|
||||
func (i *ForwardBindingIndex) buildForExpressionBody(ctx fql.IForExpressionBodyContext, scope forwardBindingScope) {
|
||||
if i == nil || ctx == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if stmt := ctx.ForExpressionStatement(); stmt != nil {
|
||||
i.buildForExpressionStatement(stmt, scope)
|
||||
return
|
||||
}
|
||||
|
||||
if clause := ctx.ForExpressionClause(); clause != nil {
|
||||
i.buildForExpressionClause(clause, scope)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *ForwardBindingIndex) buildForExpressionStatement(ctx fql.IForExpressionStatementContext, scope forwardBindingScope) {
|
||||
if i == nil || ctx == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if decl := ctx.VariableDeclaration(); decl != nil {
|
||||
i.buildVariableDeclaration(decl, scope)
|
||||
return
|
||||
}
|
||||
|
||||
i.collectNestedScopes(ctx, scope)
|
||||
}
|
||||
|
||||
func (i *ForwardBindingIndex) buildForExpressionClause(ctx fql.IForExpressionClauseContext, scope forwardBindingScope) {
|
||||
if i == nil || ctx == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if collect := ctx.CollectClause(); collect != nil {
|
||||
i.buildCollectClause(collect, scope)
|
||||
return
|
||||
}
|
||||
|
||||
i.collectNestedScopes(ctx, scope)
|
||||
}
|
||||
|
||||
func (i *ForwardBindingIndex) buildForExpressionReturn(ctx fql.IForExpressionReturnContext, scope forwardBindingScope) {
|
||||
if i == nil || ctx == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if ret := ctx.ReturnExpression(); ret != nil {
|
||||
i.collectNestedScopes(ret.Expression(), scope)
|
||||
return
|
||||
}
|
||||
|
||||
if nested := ctx.ForExpression(); nested != nil {
|
||||
i.buildForExpression(nested, scope)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *ForwardBindingIndex) buildCollectClause(ctx fql.ICollectClauseContext, scope forwardBindingScope) {
|
||||
if i == nil || ctx == nil {
|
||||
return
|
||||
}
|
||||
|
||||
i.collectNestedScopes(ctx, scope)
|
||||
|
||||
if grouping := ctx.CollectGrouping(); grouping != nil {
|
||||
for _, selector := range grouping.AllCollectSelector() {
|
||||
i.recordCollectSelector(selector, scope)
|
||||
}
|
||||
}
|
||||
|
||||
if aggregator := ctx.CollectAggregator(); aggregator != nil {
|
||||
for _, selector := range aggregator.AllCollectAggregateSelector() {
|
||||
i.recordBindingIdentifier(selector.BindingIdentifier(), scope)
|
||||
}
|
||||
}
|
||||
|
||||
if projection := ctx.CollectGroupProjection(); projection != nil {
|
||||
if selector := projection.CollectSelector(); selector != nil {
|
||||
i.recordCollectSelector(selector, scope)
|
||||
} else {
|
||||
i.recordBindingIdentifier(projection.BindingIdentifier(), scope)
|
||||
}
|
||||
}
|
||||
|
||||
if counter := ctx.CollectCounter(); counter != nil {
|
||||
i.recordBindingIdentifier(counter.BindingIdentifier(), scope)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *ForwardBindingIndex) buildVariableDeclaration(ctx fql.IVariableDeclarationContext, scope forwardBindingScope) {
|
||||
if i == nil || ctx == nil {
|
||||
return
|
||||
}
|
||||
|
||||
i.collectNestedScopes(ctx.Expression(), scope)
|
||||
i.recordDeclaration(bindingDeclarationName(ctx), i.declarationContext(ctx), scope)
|
||||
}
|
||||
|
||||
func (i *ForwardBindingIndex) buildWaitForTriggerClause(ctx fql.IWaitForTriggerClauseContext, parent forwardBindingScope) {
|
||||
if i == nil || ctx == nil {
|
||||
return
|
||||
}
|
||||
|
||||
scope := i.scopeFromNode(ctx, parent.Depth+1)
|
||||
|
||||
if inline := ctx.WaitForTriggerInlineStatement(); inline != nil {
|
||||
i.buildWaitForTriggerInlineStatement(inline, scope)
|
||||
return
|
||||
}
|
||||
|
||||
for _, stmt := range ctx.AllWaitForTriggerStatement() {
|
||||
i.buildWaitForTriggerStatement(stmt, scope)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *ForwardBindingIndex) buildWaitForTriggerStatement(ctx fql.IWaitForTriggerStatementContext, scope forwardBindingScope) {
|
||||
if i == nil || ctx == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if decl := ctx.VariableDeclaration(); decl != nil {
|
||||
i.buildVariableDeclaration(decl, scope)
|
||||
return
|
||||
}
|
||||
|
||||
i.collectNestedScopes(ctx, scope)
|
||||
}
|
||||
|
||||
func (i *ForwardBindingIndex) buildWaitForTriggerInlineStatement(ctx fql.IWaitForTriggerInlineStatementContext, scope forwardBindingScope) {
|
||||
if i == nil || ctx == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if decl := ctx.VariableDeclaration(); decl != nil {
|
||||
i.buildVariableDeclaration(decl, scope)
|
||||
return
|
||||
}
|
||||
|
||||
i.collectNestedScopes(ctx, scope)
|
||||
}
|
||||
|
||||
func (i *ForwardBindingIndex) collectNestedScopes(node antlr.Tree, scope forwardBindingScope) {
|
||||
if i == nil || node == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch ctx := node.(type) {
|
||||
case *fql.FunctionDeclarationContext:
|
||||
i.buildFunctionDeclaration(ctx, scope)
|
||||
return
|
||||
case *fql.ForExpressionContext:
|
||||
i.buildForExpression(ctx, scope)
|
||||
return
|
||||
case *fql.WaitForTriggerClauseContext:
|
||||
i.buildWaitForTriggerClause(ctx, scope)
|
||||
return
|
||||
case *fql.VariableDeclarationContext:
|
||||
i.buildVariableDeclaration(ctx, scope)
|
||||
return
|
||||
}
|
||||
|
||||
for childIdx := 0; childIdx < node.GetChildCount(); childIdx++ {
|
||||
i.collectNestedScopes(node.GetChild(childIdx), scope)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *ForwardBindingIndex) recordCollectSelector(selector fql.ICollectSelectorContext, scope forwardBindingScope) {
|
||||
if i == nil || selector == nil {
|
||||
return
|
||||
}
|
||||
|
||||
i.recordBindingIdentifier(selector.BindingIdentifier(), scope)
|
||||
}
|
||||
|
||||
func (i *ForwardBindingIndex) recordBindingIdentifier(id fql.IBindingIdentifierContext, scope forwardBindingScope) {
|
||||
if i == nil || id == nil {
|
||||
return
|
||||
}
|
||||
|
||||
i.recordDeclaration(textOfBindingIdentifier(id), i.ruleContext(id), scope)
|
||||
}
|
||||
|
||||
func (i *ForwardBindingIndex) recordDeclaration(name string, ctx antlr.ParserRuleContext, scope forwardBindingScope) {
|
||||
if i == nil || name == "" || name == core.IgnorePseudoVariable || ctx == nil || !i.scopeValid(scope) {
|
||||
return
|
||||
}
|
||||
|
||||
i.declarations = append(i.declarations, forwardBindingDeclaration{
|
||||
Name: name,
|
||||
Span: parserd.SpanFromRuleContext(ctx),
|
||||
Ctx: ctx,
|
||||
Scope: scope,
|
||||
})
|
||||
}
|
||||
|
||||
func (i *ForwardBindingIndex) declarationContext(ctx fql.IVariableDeclarationContext) antlr.ParserRuleContext {
|
||||
if i == nil || ctx == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if id := ctx.BindingIdentifier(); id != nil {
|
||||
return i.ruleContext(id)
|
||||
}
|
||||
|
||||
return i.ruleContext(ctx)
|
||||
}
|
||||
|
||||
func (i *ForwardBindingIndex) loopScope(ctx fql.IForExpressionContext, parent forwardBindingScope) forwardBindingScope {
|
||||
scope := i.scopeFromNode(ctx, parent.Depth+1)
|
||||
if ctx == nil || ctx.ForExpressionSource() == nil {
|
||||
return scope
|
||||
}
|
||||
|
||||
srcCtx := i.ruleContext(ctx.ForExpressionSource())
|
||||
if srcCtx == nil {
|
||||
return scope
|
||||
}
|
||||
|
||||
srcSpan := parserd.SpanFromRuleContext(srcCtx)
|
||||
if srcSpan.End > scope.Span.Start && srcSpan.End < scope.Span.End {
|
||||
scope.Span.Start = srcSpan.End
|
||||
}
|
||||
|
||||
return scope
|
||||
}
|
||||
|
||||
func (i *ForwardBindingIndex) scopeFromNode(node any, depth int) forwardBindingScope {
|
||||
return i.scopeFromContext(i.ruleContext(node), depth)
|
||||
}
|
||||
|
||||
func (i *ForwardBindingIndex) scopeFromContext(ctx antlr.ParserRuleContext, depth int) forwardBindingScope {
|
||||
if ctx == nil {
|
||||
return forwardBindingScope{Depth: depth}
|
||||
}
|
||||
|
||||
return forwardBindingScope{
|
||||
Span: parserd.SpanFromRuleContext(ctx),
|
||||
Depth: depth,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *ForwardBindingIndex) ruleContext(node any) antlr.ParserRuleContext {
|
||||
if ctx, ok := node.(antlr.ParserRuleContext); ok {
|
||||
return ctx
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *ForwardBindingIndex) scopeContains(scope forwardBindingScope, pos int) bool {
|
||||
return i.scopeValid(scope) && pos >= scope.Span.Start && pos < scope.Span.End
|
||||
}
|
||||
|
||||
func (i *ForwardBindingIndex) scopeValid(scope forwardBindingScope) bool {
|
||||
return scope.Span.End > scope.Span.Start
|
||||
}
|
||||
@@ -21,6 +21,7 @@ type CompilationFrontend struct {
|
||||
UDFs *UDFCompiler
|
||||
Wait *WaitCompiler
|
||||
NameCollisions *NameCollisionValidator
|
||||
ForwardBindings *ForwardBindingIndex
|
||||
}
|
||||
|
||||
func NewCompilationFrontend(session *CompilationSession) *CompilationFrontend {
|
||||
@@ -37,6 +38,7 @@ func NewCompilationFrontend(session *CompilationSession) *CompilationFrontend {
|
||||
Wait: NewWaitCompiler(session),
|
||||
Dispatch: NewDispatchCompiler(session),
|
||||
NameCollisions: NewNameCollisionValidator(session),
|
||||
ForwardBindings: NewForwardBindingIndex(session),
|
||||
TypeFacts: NewTypeFacts(session),
|
||||
CaptureAnalyzer: NewCaptureAnalyzer(session),
|
||||
UDFs: NewUDFCompiler(session),
|
||||
@@ -44,6 +46,8 @@ func NewCompilationFrontend(session *CompilationSession) *CompilationFrontend {
|
||||
Recovery: NewRecoveryCompiler(session),
|
||||
}
|
||||
|
||||
session.Program.ForwardBindings = front.ForwardBindings
|
||||
|
||||
front.Bindings.bind(front.Expressions, front.TypeFacts)
|
||||
front.Literals.bind(front.Expressions, front.TypeFacts)
|
||||
front.CaptureAnalyzer.bind(front.Bindings)
|
||||
|
||||
@@ -119,7 +119,7 @@ func (c *CollectCompiler) compileDefaultGroupProjection(kv *core.KV, identifier
|
||||
binding, found := c.ctx.Function.Symbols.ResolveBinding(varName)
|
||||
|
||||
if !found {
|
||||
c.ctx.Program.Errors.VariableNotFound(variable.GetSymbol(), varName)
|
||||
reportVariableNotFound(c.ctx, variable.GetSymbol(), varName)
|
||||
noneReg := c.ctx.Function.Registers.Allocate()
|
||||
c.ctx.Program.Emitter.EmitA(bytecode.OpLoadNone, noneReg)
|
||||
c.ctx.Function.Types.Set(noneReg, core.TypeNone)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"github.com/MontFerret/ferret/v2/pkg/bytecode"
|
||||
"github.com/MontFerret/ferret/v2/pkg/compiler/internal/core"
|
||||
"github.com/MontFerret/ferret/v2/pkg/runtime"
|
||||
"github.com/MontFerret/ferret/v2/pkg/source"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -26,6 +27,7 @@ type (
|
||||
BuildProtected func(recoveryLabel, timeoutLabel, endLabel core.Label) ProtectedRecoveryRegion
|
||||
ShouldBuildProtected func(plan core.RecoveryPlan) bool
|
||||
CompileFinalAttempt func(plan core.RecoveryPlan) bytecode.Operand
|
||||
TimeoutSpan source.Span
|
||||
JumpMode core.CatchJumpMode
|
||||
Options core.RecoveryPlanOptions
|
||||
}
|
||||
@@ -123,9 +125,9 @@ func (c *RecoveryCompiler) compileDirectOperation(plan core.RecoveryPlan, spec O
|
||||
|
||||
switch plan.OnError.ActionKind {
|
||||
case core.RecoveryActionReturn:
|
||||
return c.compileOperationWithErrorReturn(plan, buildProtected)
|
||||
return c.compileOperationWithErrorReturn(plan, spec.TimeoutSpan, buildProtected)
|
||||
case core.RecoveryActionRetry:
|
||||
return c.compileOperationWithErrorRetry(plan, buildProtected, func() bytecode.Operand {
|
||||
return c.compileOperationWithErrorRetry(plan, spec.TimeoutSpan, buildProtected, func() bytecode.Operand {
|
||||
return c.compileOperationFinalAttempt(spec, core.RecoveryPlan{OnTimeout: plan.OnTimeout})
|
||||
})
|
||||
default:
|
||||
@@ -140,9 +142,9 @@ func (c *RecoveryCompiler) compileProtectedOperation(plan core.RecoveryPlan, spe
|
||||
|
||||
switch plan.OnError.ActionKind {
|
||||
case core.RecoveryActionReturn:
|
||||
return c.compileOperationWithErrorReturn(plan, spec.BuildProtected)
|
||||
return c.compileOperationWithErrorReturn(plan, spec.TimeoutSpan, spec.BuildProtected)
|
||||
case core.RecoveryActionRetry:
|
||||
return c.compileOperationWithErrorRetry(plan, spec.BuildProtected, func() bytecode.Operand {
|
||||
return c.compileOperationWithErrorRetry(plan, spec.TimeoutSpan, spec.BuildProtected, func() bytecode.Operand {
|
||||
return c.compileOperationFinalAttempt(spec, core.RecoveryPlan{OnTimeout: plan.OnTimeout})
|
||||
})
|
||||
default:
|
||||
@@ -156,7 +158,7 @@ func (c *RecoveryCompiler) compileOperationFinalAttempt(spec OperationRecoverySp
|
||||
}
|
||||
|
||||
if plan.OnTimeout != nil && spec.CompileTimeoutAware != nil {
|
||||
return c.compileWithTimeoutRecovery(plan, spec.CompileTimeoutAware)
|
||||
return c.compileWithTimeoutRecovery(plan, spec.TimeoutSpan, spec.CompileTimeoutAware)
|
||||
}
|
||||
|
||||
if spec.CompilePlain != nil {
|
||||
@@ -168,6 +170,7 @@ func (c *RecoveryCompiler) compileOperationFinalAttempt(spec OperationRecoverySp
|
||||
|
||||
func (c *RecoveryCompiler) compileWithTimeoutRecovery(
|
||||
plan core.RecoveryPlan,
|
||||
timeoutSpan source.Span,
|
||||
compile func(timeoutLabel, endLabel core.Label) bytecode.Operand,
|
||||
) bytecode.Operand {
|
||||
if compile == nil {
|
||||
@@ -182,7 +185,7 @@ func (c *RecoveryCompiler) compileWithTimeoutRecovery(
|
||||
return out
|
||||
}
|
||||
|
||||
c.emitTimeoutHandler(out, plan, timeoutLabel, endLabel)
|
||||
c.emitTimeoutHandler(out, plan, timeoutSpan, timeoutLabel, endLabel)
|
||||
c.ctx.Program.Emitter.MarkLabel(endLabel)
|
||||
|
||||
return c.WidenResultType(out, plan)
|
||||
@@ -217,6 +220,7 @@ func (c *RecoveryCompiler) directProtectedRegionBuilder(compile func() bytecode.
|
||||
|
||||
func (c *RecoveryCompiler) compileOperationWithErrorReturn(
|
||||
plan core.RecoveryPlan,
|
||||
timeoutSpan source.Span,
|
||||
buildProtected func(recoveryLabel, timeoutLabel, endLabel core.Label) ProtectedRecoveryRegion,
|
||||
) bytecode.Operand {
|
||||
recoveryLabel := c.ctx.Program.Emitter.NewLabel("recovery", "error", "handle")
|
||||
@@ -236,7 +240,7 @@ func (c *RecoveryCompiler) compileOperationWithErrorReturn(
|
||||
c.ctx.Program.Emitter.EmitJump(endLabel)
|
||||
|
||||
if region.HasTimeout {
|
||||
c.emitTimeoutHandler(region.Result, plan, timeoutLabel, endLabel)
|
||||
c.emitTimeoutHandler(region.Result, plan, timeoutSpan, timeoutLabel, endLabel)
|
||||
}
|
||||
|
||||
c.ctx.Program.Emitter.MarkLabel(endLabel)
|
||||
@@ -248,6 +252,7 @@ func (c *RecoveryCompiler) compileOperationWithErrorReturn(
|
||||
|
||||
func (c *RecoveryCompiler) compileOperationWithErrorRetry(
|
||||
plan core.RecoveryPlan,
|
||||
timeoutSpan source.Span,
|
||||
buildProtected func(recoveryLabel, timeoutLabel, endLabel core.Label) ProtectedRecoveryRegion,
|
||||
compileFinalAttempt func() bytecode.Operand,
|
||||
) bytecode.Operand {
|
||||
@@ -323,7 +328,7 @@ func (c *RecoveryCompiler) compileOperationWithErrorRetry(
|
||||
}
|
||||
|
||||
if region.HasTimeout {
|
||||
c.emitTimeoutHandler(resultReg, plan, timeoutLabel, endLabel)
|
||||
c.emitTimeoutHandler(resultReg, plan, timeoutSpan, timeoutLabel, endLabel)
|
||||
}
|
||||
|
||||
if retry.FinalActionKind != core.RecoveryActionReturn {
|
||||
@@ -346,6 +351,7 @@ func (c *RecoveryCompiler) compileOperationWithErrorRetry(
|
||||
func (c *RecoveryCompiler) emitTimeoutHandler(
|
||||
result bytecode.Operand,
|
||||
plan core.RecoveryPlan,
|
||||
timeoutSpan source.Span,
|
||||
timeoutLabel, endLabel core.Label,
|
||||
) {
|
||||
c.ctx.Program.Emitter.MarkLabel(timeoutLabel)
|
||||
@@ -356,7 +362,9 @@ func (c *RecoveryCompiler) emitTimeoutHandler(
|
||||
c.facts.EmitMoveAuto(ensureOperandRegister(c.ctx, c.facts, result), ensureOperandRegister(c.ctx, c.facts, fallback))
|
||||
c.ctx.Program.Emitter.EmitJump(endLabel)
|
||||
default:
|
||||
c.ctx.Program.Emitter.Emit(bytecode.OpFailTimeout)
|
||||
c.ctx.Program.Emitter.WithSpan(timeoutFailureSpan(plan, timeoutSpan), func() {
|
||||
c.ctx.Program.Emitter.Emit(bytecode.OpFailTimeout)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@ package internal
|
||||
|
||||
import (
|
||||
"github.com/MontFerret/ferret/v2/pkg/compiler/internal/core"
|
||||
parserd "github.com/MontFerret/ferret/v2/pkg/parser/diagnostics"
|
||||
"github.com/MontFerret/ferret/v2/pkg/parser/fql"
|
||||
"github.com/MontFerret/ferret/v2/pkg/source"
|
||||
)
|
||||
|
||||
func missingRecoveryActionMessage(condition core.RecoveryCondition) string {
|
||||
@@ -56,6 +58,28 @@ func recoveryHandlerRetries(handler *core.RecoveryHandler) bool {
|
||||
return handler != nil && handler.ActionKind == core.RecoveryActionRetry && handler.Retry != nil
|
||||
}
|
||||
|
||||
func timeoutFailureSpan(plan core.RecoveryPlan, fallback source.Span) source.Span {
|
||||
if plan.OnTimeout == nil || plan.OnTimeout.ActionKind != core.RecoveryActionFail {
|
||||
return fallback
|
||||
}
|
||||
|
||||
if plan.OnTimeout.TailNode != nil {
|
||||
span := parserd.SpanFromRuleContext(plan.OnTimeout.TailNode)
|
||||
if span.End > span.Start {
|
||||
return span
|
||||
}
|
||||
}
|
||||
|
||||
if plan.OnTimeout.ActionNode != nil {
|
||||
span := parserd.SpanFromRuleContext(plan.OnTimeout.ActionNode)
|
||||
if span.End > span.Start {
|
||||
return span
|
||||
}
|
||||
}
|
||||
|
||||
return fallback
|
||||
}
|
||||
|
||||
func isNoneLiteralExpression(expr fql.IExpressionContext) bool {
|
||||
if expr == nil || expr.UnaryOperator() != nil || expr.LogicalAndOperator() != nil || expr.LogicalOrOperator() != nil || expr.GetTernaryOperator() != nil {
|
||||
return false
|
||||
|
||||
@@ -196,19 +196,23 @@ func literalTruthinessFromExpression(ctx fql.IExpressionContext) (bool, bool) {
|
||||
}
|
||||
|
||||
func waitForHasExplicitTimeoutClause(ctx fql.IWaitForExpressionContext) bool {
|
||||
return waitForTimeoutClause(ctx) != nil
|
||||
}
|
||||
|
||||
func waitForTimeoutClause(ctx fql.IWaitForExpressionContext) fql.ITimeoutClauseContext {
|
||||
if ctx == nil {
|
||||
return false
|
||||
return nil
|
||||
}
|
||||
|
||||
if ev := ctx.WaitForEventExpression(); ev != nil && waitForEventTimeoutClause(ev) != nil {
|
||||
return true
|
||||
if ev := ctx.WaitForEventExpression(); ev != nil {
|
||||
return waitForEventTimeoutClause(ev)
|
||||
}
|
||||
|
||||
if pred := ctx.WaitForPredicateExpression(); pred != nil && pred.TimeoutClause() != nil {
|
||||
return true
|
||||
if pred := ctx.WaitForPredicateExpression(); pred != nil {
|
||||
return pred.TimeoutClause()
|
||||
}
|
||||
|
||||
return false
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseDurationLiteral(text string) (runtime.Value, error) {
|
||||
|
||||
@@ -70,6 +70,10 @@ func (c *WaitCompiler) newWaitOperationRecoverySpec(ctx fql.IWaitForExpressionCo
|
||||
return spec
|
||||
}
|
||||
|
||||
if timeout := waitForTimeoutClause(ctx); timeout != nil {
|
||||
spec.TimeoutSpan = waitForSpan(timeout, nil)
|
||||
}
|
||||
|
||||
if ev := ctx.WaitForEventExpression(); ev != nil {
|
||||
spec.CompilePlain = func() bytecode.Operand {
|
||||
return c.compileEvent(ev)
|
||||
|
||||
@@ -33,6 +33,7 @@ func (v *Visitor) VisitProgram(ctx *fql.ProgramContext) interface{} {
|
||||
}
|
||||
|
||||
v.Frontend.UDFCatalog.BuildCatalog(ctx)
|
||||
v.Frontend.ForwardBindings.BuildProgram(ctx)
|
||||
v.Frontend.NameCollisions.ValidateProgram(ctx)
|
||||
if ctx != nil {
|
||||
if body, ok := ctx.Body().(*fql.BodyContext); ok {
|
||||
|
||||
@@ -115,6 +115,26 @@ func (h *ErrorHandler) VariableNotFound(token antlr.Token, name string) {
|
||||
})
|
||||
}
|
||||
|
||||
// VariableUsedBeforeDeclaration reports a binding reference that resolves to a
|
||||
// declaration which appears later in the same visible lexical scope.
|
||||
func (h *ErrorHandler) VariableUsedBeforeDeclaration(token antlr.Token, name string, declaration antlr.ParserRuleContext) {
|
||||
spans := []diagnostics.ErrorSpan{
|
||||
diagnostics.NewMainErrorSpan(SpanFromToken(token), "used before declaration"),
|
||||
}
|
||||
|
||||
if declaration != nil {
|
||||
spans = append(spans, diagnostics.NewSecondaryErrorSpan(SpanFromRuleContext(declaration), "declared later"))
|
||||
}
|
||||
|
||||
h.Add(&diagnostics.Diagnostic{
|
||||
Message: fmt.Sprintf("Variable '%s' is used before declaration", name),
|
||||
Source: h.src,
|
||||
Spans: spans,
|
||||
Kind: NameError,
|
||||
Hint: "Move the declaration before this use.",
|
||||
})
|
||||
}
|
||||
|
||||
func (h *ErrorHandler) DuplicateMatchBinding(ctx antlr.ParserRuleContext, name string) {
|
||||
h.Add(&diagnostics.Diagnostic{
|
||||
Message: fmt.Sprintf("duplicate binding '%s' in MATCH pattern", name),
|
||||
|
||||
@@ -3,10 +3,18 @@ package diagnostics
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/antlr4-go/antlr/v4"
|
||||
|
||||
"github.com/MontFerret/ferret/v2/pkg/diagnostics"
|
||||
"github.com/MontFerret/ferret/v2/pkg/source"
|
||||
)
|
||||
|
||||
const (
|
||||
filterAssignmentMessage = "Assignment is not valid in a FILTER predicate"
|
||||
filterComparisonAssignmentHint = "Use '==' to compare values, e.g. FILTER user.active == true."
|
||||
filterStatementAssignmentHint = "Move the assignment to a standalone statement before FILTER. FILTER predicates must be expressions."
|
||||
)
|
||||
|
||||
func matchMissingAssignmentValue(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
|
||||
if matchInvalidVarDiscard(src, err, offending) {
|
||||
return true
|
||||
@@ -89,6 +97,10 @@ func matchAssignmentExpression(src *source.Source, err *diagnostics.Diagnostic,
|
||||
return false
|
||||
}
|
||||
|
||||
if matchFilterAssignmentExpression(src, err, offending) {
|
||||
return true
|
||||
}
|
||||
|
||||
operator := findPrevAssignmentOperator(offending, 16)
|
||||
if operator == nil {
|
||||
return false
|
||||
@@ -120,6 +132,495 @@ func matchAssignmentExpression(src *source.Source, err *diagnostics.Diagnostic,
|
||||
return true
|
||||
}
|
||||
|
||||
func matchFilterAssignmentExpression(src *source.Source, err *diagnostics.Diagnostic, offending *TokenNode) bool {
|
||||
if src == nil || err == nil || offending == nil || offending.Token() == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
tokens := lexDefaultTokens(src.Content())
|
||||
offendingIdx := findFilterAssignmentOffendingIndex(tokens, offending)
|
||||
if offendingIdx < 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
filterIdx := findFilterPredicateStart(tokens, offendingIdx)
|
||||
if filterIdx < 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
predicateStartIdx := filterIdx + 1
|
||||
predicateEndIdx := findFilterPredicateEnd(tokens, predicateStartIdx)
|
||||
if offendingIdx < predicateStartIdx || offendingIdx > predicateEndIdx {
|
||||
return false
|
||||
}
|
||||
|
||||
operatorIdx := findFilterAssignmentOperator(tokens, predicateStartIdx, predicateEndIdx, offendingIdx)
|
||||
if operatorIdx < 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
operator := tokens[operatorIdx]
|
||||
span := spanFromTokenSafe(operator, src)
|
||||
label := "assignment is not an expression"
|
||||
hint := filterStatementAssignmentHint
|
||||
|
||||
if isTokenText(operator, "=") {
|
||||
label = "use '==' for comparison"
|
||||
hint = filterComparisonAssignmentHint
|
||||
}
|
||||
|
||||
err.Message = filterAssignmentMessage
|
||||
err.Hint = hint
|
||||
err.Spans = []diagnostics.ErrorSpan{
|
||||
diagnostics.NewMainErrorSpan(span, label),
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func findFilterAssignmentOffendingIndex(tokens []antlr.Token, offending *TokenNode) int {
|
||||
for node := offending; node != nil; node = node.Prev() {
|
||||
idx := findLexedTokenIndex(tokens, node.Token())
|
||||
if idx >= 0 {
|
||||
return idx
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func findFilterPredicateEnd(tokens []antlr.Token, startIdx int) int {
|
||||
depth := 0
|
||||
|
||||
for i := startIdx; i < len(tokens); i++ {
|
||||
text := tokenText(tokens[i])
|
||||
|
||||
switch text {
|
||||
case "(", "[", "{":
|
||||
depth++
|
||||
case ")", "]", "}":
|
||||
if depth == 0 {
|
||||
return i
|
||||
}
|
||||
|
||||
depth--
|
||||
default:
|
||||
if depth == 0 && isFilterPredicateBoundary(text) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return len(tokens)
|
||||
}
|
||||
|
||||
func findFilterPredicateStart(tokens []antlr.Token, offendingIdx int) int {
|
||||
depth := 0
|
||||
|
||||
for i := offendingIdx; i >= 0; i-- {
|
||||
text := tokenText(tokens[i])
|
||||
|
||||
switch text {
|
||||
case ")", "]", "}":
|
||||
depth++
|
||||
case "(", "[", "{":
|
||||
if depth > 0 {
|
||||
depth--
|
||||
}
|
||||
default:
|
||||
if depth != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if text == "FILTER" {
|
||||
return i
|
||||
}
|
||||
|
||||
if i != offendingIdx && isFilterPredicateBoundary(text) {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func findFilterAssignmentOperator(tokens []antlr.Token, startIdx, endIdx, offendingIdx int) int {
|
||||
if startIdx < 0 || endIdx > len(tokens) || startIdx >= endIdx || offendingIdx < startIdx {
|
||||
return -1
|
||||
}
|
||||
|
||||
if offendingIdx >= endIdx {
|
||||
offendingIdx = endIdx - 1
|
||||
}
|
||||
|
||||
if operatorIdx := findForwardFilterAssignmentOperator(tokens, startIdx, endIdx, offendingIdx); operatorIdx >= 0 {
|
||||
return operatorIdx
|
||||
}
|
||||
|
||||
return findBackwardFilterAssignmentOperator(tokens, startIdx, endIdx, offendingIdx)
|
||||
}
|
||||
|
||||
func findForwardFilterAssignmentOperator(tokens []antlr.Token, startIdx, endIdx, offendingIdx int) int {
|
||||
depth := 0
|
||||
|
||||
for i := offendingIdx; i < endIdx; i++ {
|
||||
text := tokenText(tokens[i])
|
||||
|
||||
switch text {
|
||||
case "(", "[", "{":
|
||||
depth++
|
||||
case ")", "]", "}":
|
||||
if depth == 0 {
|
||||
return -1
|
||||
}
|
||||
|
||||
depth--
|
||||
default:
|
||||
if depth == 0 && i != offendingIdx && isFilterAssignmentSearchBoundary(tokens, i) {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
if !isAssignmentOperatorText(text) {
|
||||
continue
|
||||
}
|
||||
|
||||
if isFilterAssignmentOperatorCandidate(tokens, startIdx, endIdx, i) {
|
||||
return i
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func findBackwardFilterAssignmentOperator(tokens []antlr.Token, startIdx, endIdx, offendingIdx int) int {
|
||||
depth := 0
|
||||
|
||||
for i := offendingIdx; i >= startIdx; i-- {
|
||||
text := tokenText(tokens[i])
|
||||
|
||||
switch text {
|
||||
case ")", "]", "}":
|
||||
depth++
|
||||
case "(", "[", "{":
|
||||
if depth == 0 {
|
||||
return -1
|
||||
}
|
||||
depth--
|
||||
default:
|
||||
if depth == 0 && i != offendingIdx && isFilterAssignmentSearchBoundary(tokens, i) {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
if !isAssignmentOperatorText(text) {
|
||||
continue
|
||||
}
|
||||
|
||||
if isFilterAssignmentOperatorCandidate(tokens, startIdx, endIdx, i) {
|
||||
return i
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func isFilterAssignmentOperatorCandidate(tokens []antlr.Token, startIdx, endIdx, operatorIdx int) bool {
|
||||
if isFilterDeclarationAssignmentOperator(tokens, operatorIdx) {
|
||||
return false
|
||||
}
|
||||
|
||||
if !hasFilterAssignmentValue(tokens, operatorIdx+1, endIdx) {
|
||||
return false
|
||||
}
|
||||
|
||||
targetStartIdx := findFilterAssignmentTargetStart(tokens, startIdx, operatorIdx)
|
||||
return targetStartIdx >= 0 && isFilterAssignmentTarget(tokens, targetStartIdx, operatorIdx)
|
||||
}
|
||||
|
||||
func isFilterAssignmentSearchBoundary(tokens []antlr.Token, idx int) bool {
|
||||
text := tokenText(tokens[idx])
|
||||
if isFilterPredicateBoundary(text) {
|
||||
return true
|
||||
}
|
||||
|
||||
switch text {
|
||||
case ",", ":", "AND", "OR", "&&", "||":
|
||||
return true
|
||||
case "?":
|
||||
return idx+1 >= len(tokens) || !isTokenText(tokens[idx+1], ".")
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isFilterDeclarationAssignmentOperator(tokens []antlr.Token, operatorIdx int) bool {
|
||||
if operatorIdx < 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
switch tokenText(tokens[operatorIdx-2]) {
|
||||
case "LET", "VAR", "COLLECT", "AGGREGATE", "FOR":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func findFilterAssignmentTargetStart(tokens []antlr.Token, startIdx, operatorIdx int) int {
|
||||
if startIdx < 0 || startIdx >= operatorIdx {
|
||||
return -1
|
||||
}
|
||||
|
||||
depth := 0
|
||||
|
||||
for i := operatorIdx - 1; i >= startIdx; i-- {
|
||||
text := tokenText(tokens[i])
|
||||
|
||||
switch text {
|
||||
case ")", "]", "}":
|
||||
depth++
|
||||
case "(", "[", "{":
|
||||
if depth == 0 {
|
||||
return i + 1
|
||||
}
|
||||
|
||||
depth--
|
||||
default:
|
||||
if depth == 0 && isFilterAssignmentTargetBoundary(tokens, i) {
|
||||
return i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return startIdx
|
||||
}
|
||||
|
||||
func isFilterAssignmentTargetBoundary(tokens []antlr.Token, idx int) bool {
|
||||
text := tokenText(tokens[idx])
|
||||
if isFilterPredicateBoundary(text) {
|
||||
return true
|
||||
}
|
||||
|
||||
switch text {
|
||||
case ",", ":", "AND", "OR", "&&", "||":
|
||||
return true
|
||||
case "?":
|
||||
return idx+1 >= len(tokens) || !isTokenText(tokens[idx+1], ".")
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func hasFilterAssignmentValue(tokens []antlr.Token, startIdx, endIdx int) bool {
|
||||
if startIdx >= endIdx {
|
||||
return false
|
||||
}
|
||||
|
||||
depth := 0
|
||||
seenValue := false
|
||||
|
||||
for i := startIdx; i < endIdx; i++ {
|
||||
text := tokenText(tokens[i])
|
||||
|
||||
switch text {
|
||||
case "(", "[", "{":
|
||||
depth++
|
||||
seenValue = true
|
||||
case ")", "]", "}":
|
||||
if depth == 0 {
|
||||
return seenValue
|
||||
}
|
||||
|
||||
depth--
|
||||
case ",":
|
||||
if depth == 0 {
|
||||
return seenValue
|
||||
}
|
||||
default:
|
||||
if depth == 0 && isFilterPredicateBoundary(text) {
|
||||
return seenValue
|
||||
}
|
||||
|
||||
seenValue = true
|
||||
if depth == 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return seenValue && depth == 0
|
||||
}
|
||||
|
||||
func isFilterAssignmentTarget(tokens []antlr.Token, startIdx, operatorIdx int) bool {
|
||||
if startIdx >= operatorIdx || startIdx < 0 || operatorIdx > len(tokens) {
|
||||
return false
|
||||
}
|
||||
|
||||
if isTokenText(tokens[startIdx], ".") || isFilterAssignmentSafeImplicitTargetStart(tokens, startIdx) {
|
||||
return isFilterImplicitAssignmentTarget(tokens, startIdx, operatorIdx)
|
||||
}
|
||||
|
||||
if !isFilterAssignmentIdentifier(tokens[startIdx]) {
|
||||
return false
|
||||
}
|
||||
|
||||
return isFilterAssignmentPathTail(tokens, startIdx+1, operatorIdx)
|
||||
}
|
||||
|
||||
func isFilterImplicitAssignmentTarget(tokens []antlr.Token, startIdx, operatorIdx int) bool {
|
||||
i := startIdx
|
||||
|
||||
if isTokenText(tokens[i], "?") {
|
||||
i++
|
||||
}
|
||||
|
||||
if i >= operatorIdx || !isTokenText(tokens[i], ".") {
|
||||
return false
|
||||
}
|
||||
|
||||
i++
|
||||
if i >= operatorIdx {
|
||||
return false
|
||||
}
|
||||
|
||||
if isTokenText(tokens[i], "[") {
|
||||
closeIdx := findFilterAssignmentComputedEnd(tokens, i, operatorIdx)
|
||||
if closeIdx < 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
i = closeIdx + 1
|
||||
} else {
|
||||
if !isFilterAssignmentPathToken(tokens[i]) {
|
||||
return false
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
return isFilterAssignmentPathTail(tokens, i, operatorIdx)
|
||||
}
|
||||
|
||||
func isFilterAssignmentSafeImplicitTargetStart(tokens []antlr.Token, startIdx int) bool {
|
||||
return isTokenText(tokens[startIdx], "?") && startIdx+1 < len(tokens) && isTokenText(tokens[startIdx+1], ".")
|
||||
}
|
||||
|
||||
func isFilterAssignmentPathTail(tokens []antlr.Token, startIdx, operatorIdx int) bool {
|
||||
for i := startIdx; i < operatorIdx; {
|
||||
text := tokenText(tokens[i])
|
||||
|
||||
switch text {
|
||||
case ".":
|
||||
i++
|
||||
if i >= operatorIdx || !isFilterAssignmentPathToken(tokens[i]) {
|
||||
return false
|
||||
}
|
||||
|
||||
i++
|
||||
case "?":
|
||||
if i+1 >= operatorIdx || !isTokenText(tokens[i+1], ".") {
|
||||
return false
|
||||
}
|
||||
|
||||
i += 2
|
||||
if i >= operatorIdx {
|
||||
return false
|
||||
}
|
||||
|
||||
if isTokenText(tokens[i], "[") {
|
||||
closeIdx := findFilterAssignmentComputedEnd(tokens, i, operatorIdx)
|
||||
if closeIdx < 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
i = closeIdx + 1
|
||||
continue
|
||||
}
|
||||
|
||||
if !isFilterAssignmentPathToken(tokens[i]) {
|
||||
return false
|
||||
}
|
||||
|
||||
i++
|
||||
case "[":
|
||||
closeIdx := findFilterAssignmentComputedEnd(tokens, i, operatorIdx)
|
||||
if closeIdx < 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
i = closeIdx + 1
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func findFilterAssignmentComputedEnd(tokens []antlr.Token, openIdx, limitIdx int) int {
|
||||
depth := 0
|
||||
|
||||
for i := openIdx; i < limitIdx; i++ {
|
||||
text := tokenText(tokens[i])
|
||||
|
||||
switch text {
|
||||
case "(", "[", "{":
|
||||
depth++
|
||||
case ")", "]", "}":
|
||||
depth--
|
||||
if depth < 0 {
|
||||
return -1
|
||||
}
|
||||
|
||||
if depth == 0 {
|
||||
if delimitersMatch(tokenText(tokens[openIdx]), text) {
|
||||
return i
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func isFilterAssignmentIdentifier(token antlr.Token) bool {
|
||||
if token == nil || tokenText(token) == "_" {
|
||||
return false
|
||||
}
|
||||
|
||||
return isLoopVariableToken(&TokenNode{token: token})
|
||||
}
|
||||
|
||||
func isFilterAssignmentPathToken(token antlr.Token) bool {
|
||||
if token == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
switch tokenText(token) {
|
||||
case "", "(", ")", "[", "]", "{", "}", ".", "?", ",", ":", "+", "-", "*", "/", "%", "=", "==", "!=", ">", "<", ">=", "<=", "=~", "!~", "=>", "<-", "&&", "||", "AND", "OR":
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func isFilterPredicateBoundary(text string) bool {
|
||||
switch text {
|
||||
case "RETURN", "FOR", "FILTER", "SORT", "LIMIT", "COLLECT", "LET", "VAR", "DELETE", "WAITFOR", "DISPATCH", "FUNC", "USE":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func anyAssignmentOperator(nodes ...*TokenNode) *TokenNode {
|
||||
for _, node := range nodes {
|
||||
if isAssignmentOperator(node) {
|
||||
@@ -144,5 +645,18 @@ func findPrevAssignmentOperator(node *TokenNode, steps int) *TokenNode {
|
||||
}
|
||||
|
||||
func isAssignmentOperator(node *TokenNode) bool {
|
||||
return is(node, "=") || is(node, "+=") || is(node, "-=") || is(node, "*=") || is(node, "/=")
|
||||
if node == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return isAssignmentOperatorText(node.GetText())
|
||||
}
|
||||
|
||||
func isAssignmentOperatorText(text string) bool {
|
||||
switch text {
|
||||
case "=", "+=", "-=", "*=", "/=":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
package diagnostics
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/antlr4-go/antlr/v4"
|
||||
|
||||
pkgdiagnostics "github.com/MontFerret/ferret/v2/pkg/diagnostics"
|
||||
"github.com/MontFerret/ferret/v2/pkg/source"
|
||||
)
|
||||
|
||||
func TestMatchFilterAssignmentExpressionFlexibleTargets(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
operator string
|
||||
offending string
|
||||
expectedHint string
|
||||
expectedLabel string
|
||||
}{
|
||||
{
|
||||
name: "direct path",
|
||||
input: "FOR user IN users FILTER user.active = true RETURN user",
|
||||
operator: "=",
|
||||
expectedHint: filterComparisonAssignmentHint,
|
||||
expectedLabel: "use '==' for comparison",
|
||||
},
|
||||
{
|
||||
name: "left path offending token",
|
||||
input: "FOR user IN users FILTER user.active = true RETURN user",
|
||||
operator: "=",
|
||||
offending: ".",
|
||||
expectedHint: filterComparisonAssignmentHint,
|
||||
expectedLabel: "use '==' for comparison",
|
||||
},
|
||||
{
|
||||
name: "clause boundary offending token",
|
||||
input: "FOR user IN users FILTER user.active = true RETURN user",
|
||||
operator: "=",
|
||||
offending: "RETURN",
|
||||
expectedHint: filterComparisonAssignmentHint,
|
||||
expectedLabel: "use '==' for comparison",
|
||||
},
|
||||
{
|
||||
name: "grouped path",
|
||||
input: "FOR user IN users FILTER (user.active = true) RETURN user",
|
||||
operator: "=",
|
||||
expectedHint: filterComparisonAssignmentHint,
|
||||
expectedLabel: "use '==' for comparison",
|
||||
},
|
||||
{
|
||||
name: "logical prefix path",
|
||||
input: "FOR user IN users FILTER user.name != NONE AND (user.active = true) RETURN user",
|
||||
operator: "=",
|
||||
expectedHint: filterComparisonAssignmentHint,
|
||||
expectedLabel: "use '==' for comparison",
|
||||
},
|
||||
{
|
||||
name: "implicit current inline filter",
|
||||
input: "RETURN users[* FILTER (.active = true)]",
|
||||
operator: "=",
|
||||
expectedHint: filterComparisonAssignmentHint,
|
||||
expectedLabel: "use '==' for comparison",
|
||||
},
|
||||
{
|
||||
name: "augmented assignment",
|
||||
input: "FOR user IN users FILTER (user.active += true) RETURN user",
|
||||
operator: "+=",
|
||||
expectedHint: filterStatementAssignmentHint,
|
||||
expectedLabel: "assignment is not an expression",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
src := source.New("filter_assignment.fql", tt.input)
|
||||
tokens := lexDefaultTokens(tt.input)
|
||||
operatorIdx := findTestTokenIndex(t, tokens, tt.operator)
|
||||
offendingIdx := operatorIdx
|
||||
if tt.offending != "" {
|
||||
offendingIdx = findTestTokenIndex(t, tokens, tt.offending)
|
||||
}
|
||||
err := &pkgdiagnostics.Diagnostic{
|
||||
Kind: SyntaxError,
|
||||
Message: "no viable alternative at input",
|
||||
Source: src,
|
||||
}
|
||||
|
||||
if !matchFilterAssignmentExpression(src, err, &TokenNode{token: tokens[offendingIdx]}) {
|
||||
t.Fatal("expected FILTER assignment matcher to handle diagnostic")
|
||||
}
|
||||
|
||||
if err.Message != filterAssignmentMessage {
|
||||
t.Fatalf("message = %q, want %q", err.Message, filterAssignmentMessage)
|
||||
}
|
||||
if err.Hint != tt.expectedHint {
|
||||
t.Fatalf("hint = %q, want %q", err.Hint, tt.expectedHint)
|
||||
}
|
||||
if len(err.Spans) != 1 {
|
||||
t.Fatalf("expected 1 span, got %d", len(err.Spans))
|
||||
}
|
||||
|
||||
span := err.Spans[0]
|
||||
if !span.Main {
|
||||
t.Fatal("expected main span")
|
||||
}
|
||||
if span.Label != tt.expectedLabel {
|
||||
t.Fatalf("label = %q, want %q", span.Label, tt.expectedLabel)
|
||||
}
|
||||
|
||||
expectedSpan := spanFromTokenSafe(tokens[operatorIdx], src)
|
||||
if span.Span != expectedSpan {
|
||||
t.Fatalf("span = %#v, want %#v", span.Span, expectedSpan)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchFilterAssignmentExpressionRejectsNonCandidates(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
operator string
|
||||
}{
|
||||
{
|
||||
name: "missing value",
|
||||
input: "FOR user IN users FILTER user.active =",
|
||||
operator: "=",
|
||||
},
|
||||
{
|
||||
name: "non path left operand",
|
||||
input: "FOR user IN users FILTER (1 = 1) RETURN user",
|
||||
operator: "=",
|
||||
},
|
||||
{
|
||||
name: "nested declaration assignment",
|
||||
input: "FOR user IN users FILTER (FOR item IN user.items LET active = true RETURN active) RETURN user",
|
||||
operator: "=",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
src := source.New("filter_assignment.fql", tt.input)
|
||||
tokens := lexDefaultTokens(tt.input)
|
||||
operatorIdx := findTestTokenIndex(t, tokens, tt.operator)
|
||||
err := &pkgdiagnostics.Diagnostic{
|
||||
Kind: SyntaxError,
|
||||
Message: "no viable alternative at input",
|
||||
Source: src,
|
||||
}
|
||||
|
||||
if matchFilterAssignmentExpression(src, err, &TokenNode{token: tokens[operatorIdx]}) {
|
||||
t.Fatal("expected FILTER assignment matcher to ignore diagnostic")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func findTestTokenIndex(t *testing.T, tokens []antlr.Token, text string) int {
|
||||
t.Helper()
|
||||
|
||||
for idx, token := range tokens {
|
||||
if isTokenText(token, text) {
|
||||
return idx
|
||||
}
|
||||
}
|
||||
|
||||
t.Fatalf("token %q not found", text)
|
||||
return -1
|
||||
}
|
||||
@@ -149,6 +149,10 @@ func ToRuntimeError(program *bytecode.Program, pc int, callStack []frame.TraceEn
|
||||
spec.Label = "divisor evaluates to zero"
|
||||
spec.Hint = "Ensure the divisor is non-zero before modulo"
|
||||
spec.Cause = ErrModuloByZero
|
||||
case errors.Is(err, runtime.ErrTimeout):
|
||||
spec.Message = runtime.ErrTimeout.Error()
|
||||
spec.Label = "operation timed out here"
|
||||
spec.Cause = runtime.ErrTimeout
|
||||
case hasMemberErr:
|
||||
spec.Kind = diagnostics.TypeError
|
||||
spec.Message = "invalid type"
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
@@ -191,6 +195,18 @@ func TestForLoopSyntaxErrors(t *testing.T) {
|
||||
Hint: "FILTER requires a boolean expression.",
|
||||
}, "FILTER with no expression 2"),
|
||||
|
||||
Failure(
|
||||
`
|
||||
LET users = []
|
||||
FOR user IN users
|
||||
FILTER user.active += true
|
||||
RETURN user
|
||||
`, E{
|
||||
Kind: parserd.SyntaxError,
|
||||
Message: "Assignment is not valid in a FILTER predicate",
|
||||
Hint: "Move the assignment to a standalone statement before FILTER. FILTER predicates must be expressions.",
|
||||
}, "FILTER with augmented assignment"),
|
||||
|
||||
Failure(
|
||||
`
|
||||
LET users = []
|
||||
@@ -326,3 +342,122 @@ func TestForLoopSyntaxErrors(t *testing.T) {
|
||||
}, "COLLECT AGGREGATE without expression 2"),
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterAssignmentDiagnosticRecognizesGroupedPredicates(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
predicate string
|
||||
}{
|
||||
{
|
||||
name: "grouped predicate",
|
||||
predicate: "(user.active = true)",
|
||||
},
|
||||
{
|
||||
name: "logical prefix predicate",
|
||||
predicate: "user.name != NONE AND (user.active = true)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
query := `LET users = [
|
||||
{ name: "Ada", active: true },
|
||||
{ name: "Grace", active: false },
|
||||
{ name: "Linus", active: true }
|
||||
]
|
||||
|
||||
FOR user IN users
|
||||
FILTER ` + tt.predicate + `
|
||||
RETURN user.name`
|
||||
|
||||
_, 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 != "Assignment is not valid in a FILTER predicate" {
|
||||
t.Fatalf("unexpected diagnostic message: %q", diag.Message)
|
||||
}
|
||||
if diag.Hint != "Use '==' to compare values, e.g. FILTER user.active == true." {
|
||||
t.Fatalf("unexpected diagnostic hint: %q", diag.Hint)
|
||||
}
|
||||
if len(diag.Spans) != 1 {
|
||||
t.Fatalf("expected 1 span, got %d", len(diag.Spans))
|
||||
}
|
||||
|
||||
span := diag.Spans[0]
|
||||
if !span.Main {
|
||||
t.Fatal("expected main span")
|
||||
}
|
||||
if span.Label != "use '==' for comparison" {
|
||||
t.Fatalf("unexpected span label: %q", span.Label)
|
||||
}
|
||||
|
||||
operatorStart := strings.Index(query, " = true")
|
||||
if operatorStart < 0 {
|
||||
t.Fatal("test query is missing assignment operator")
|
||||
}
|
||||
operatorStart++
|
||||
|
||||
if span.Span.Start != operatorStart || span.Span.End != operatorStart+1 {
|
||||
t.Fatalf("unexpected operator span: %#v, want [%d,%d)", span.Span, operatorStart, operatorStart+1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterAssignmentDiagnosticPointsAtAssignmentOperator(t *testing.T) {
|
||||
query := `LET users = [
|
||||
{ name: "Ada", active: true },
|
||||
{ name: "Grace", active: false },
|
||||
{ name: "Linus", active: true }
|
||||
]
|
||||
|
||||
FOR user IN users
|
||||
FILTER user.active = true
|
||||
RETURN user.name`
|
||||
|
||||
_, 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 != "Assignment is not valid in a FILTER predicate" {
|
||||
t.Fatalf("unexpected diagnostic message: %q", diag.Message)
|
||||
}
|
||||
|
||||
if diag.Hint != "Use '==' to compare values, e.g. FILTER user.active == true." {
|
||||
t.Fatalf("unexpected diagnostic hint: %q", diag.Hint)
|
||||
}
|
||||
|
||||
formatted := pkgdiagnostics.Format(err)
|
||||
expected := `SyntaxError: Assignment is not valid in a FILTER predicate
|
||||
--> anonymous:8:24
|
||||
|
|
||||
7 | FOR user IN users
|
||||
8 | FILTER user.active = true
|
||||
| ^ use '==' for comparison
|
||||
9 | RETURN user.name
|
||||
Hint: Use '==' to compare values, e.g. FILTER user.active == true.
|
||||
`
|
||||
if formatted != expected {
|
||||
t.Fatalf("unexpected formatted diagnostic:\n%s", formatted)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
package compiler_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/MontFerret/ferret/v2/pkg/compiler"
|
||||
diagpkg "github.com/MontFerret/ferret/v2/pkg/diagnostics"
|
||||
parserd "github.com/MontFerret/ferret/v2/pkg/parser/diagnostics"
|
||||
"github.com/MontFerret/ferret/v2/pkg/source"
|
||||
)
|
||||
|
||||
func TestForwardDeclarationDiagnosticsTopLevel(t *testing.T) {
|
||||
diagnostics := compileDiagnostics(t, `
|
||||
LET total = price + tax
|
||||
LET price = 19.99
|
||||
LET tax = price * 0.08
|
||||
|
||||
RETURN total
|
||||
`)
|
||||
|
||||
if got, want := len(diagnostics), 2; got != want {
|
||||
t.Fatalf("expected %d diagnostics, got %d", want, got)
|
||||
}
|
||||
|
||||
requireForwardDeclarationDiagnostic(t, diagnostics[0], "price", 3)
|
||||
requireForwardDeclarationDiagnostic(t, diagnostics[1], "tax", 4)
|
||||
}
|
||||
|
||||
func TestForwardDeclarationDiagnosticsNestedFunctionScope(t *testing.T) {
|
||||
diagnostics := compileDiagnostics(t, `
|
||||
FUNC outer() (
|
||||
FUNC inner() => later
|
||||
LET later = 1
|
||||
RETURN inner()
|
||||
)
|
||||
RETURN outer()
|
||||
`)
|
||||
|
||||
requireForwardDeclarationDiagnostic(t, diagnostics[0], "later", 4)
|
||||
}
|
||||
|
||||
func TestForwardDeclarationDiagnosticsLoopScope(t *testing.T) {
|
||||
diagnostics := compileDiagnostics(t, `
|
||||
RETURN (
|
||||
FOR item IN [1]
|
||||
LET value = later
|
||||
LET later = item
|
||||
RETURN value
|
||||
)
|
||||
`)
|
||||
|
||||
requireForwardDeclarationDiagnostic(t, diagnostics[0], "later", 5)
|
||||
}
|
||||
|
||||
func TestForwardDeclarationDiagnosticsAssignmentAndDeleteRoots(t *testing.T) {
|
||||
assignment := compileDiagnostics(t, `
|
||||
target = 1
|
||||
LET target = 2
|
||||
RETURN target
|
||||
`)
|
||||
requireForwardDeclarationDiagnostic(t, assignment[0], "target", 3)
|
||||
|
||||
deletion := compileDiagnostics(t, `
|
||||
DELETE target.value
|
||||
LET target = { value: 1 }
|
||||
RETURN target
|
||||
`)
|
||||
requireForwardDeclarationDiagnostic(t, deletion[0], "target", 3)
|
||||
}
|
||||
|
||||
func TestForwardDeclarationDiagnosticsIgnoreDescendantScopeDeclarations(t *testing.T) {
|
||||
diagnostics := compileDiagnostics(t, `
|
||||
LET value = later
|
||||
LET ignored = (
|
||||
FOR item IN [1]
|
||||
LET later = item
|
||||
RETURN later
|
||||
)
|
||||
RETURN value
|
||||
`)
|
||||
|
||||
if got, want := diagnostics[0].Message, "Variable 'later' is not defined"; got != want {
|
||||
t.Fatalf("unexpected diagnostic message: got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func compileDiagnostics(t *testing.T, src string) []*diagpkg.Diagnostic {
|
||||
t.Helper()
|
||||
|
||||
_, err := compiler.New(compiler.WithOptimizationLevel(compiler.O0)).Compile(source.NewAnonymous(src))
|
||||
if err == nil {
|
||||
t.Fatal("expected compile diagnostics")
|
||||
}
|
||||
|
||||
switch e := err.(type) {
|
||||
case *diagpkg.Diagnostic:
|
||||
return []*diagpkg.Diagnostic{e}
|
||||
case *diagpkg.DiagnosticSet:
|
||||
out := make([]*diagpkg.Diagnostic, 0, e.Size())
|
||||
for idx := 0; idx < e.Size(); idx++ {
|
||||
out = append(out, e.Get(idx))
|
||||
}
|
||||
return out
|
||||
default:
|
||||
t.Fatalf("unexpected error type: %T", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func requireForwardDeclarationDiagnostic(t *testing.T, actual *diagpkg.Diagnostic, name string, declarationLine int) {
|
||||
t.Helper()
|
||||
|
||||
if actual == nil {
|
||||
t.Fatal("expected diagnostic")
|
||||
}
|
||||
|
||||
if actual.Kind != parserd.NameError {
|
||||
t.Fatalf("unexpected diagnostic kind: got %s, want %s", actual.Kind, parserd.NameError)
|
||||
}
|
||||
|
||||
if got, want := actual.Message, fmt.Sprintf("Variable '%s' is used before declaration", name); got != want {
|
||||
t.Fatalf("unexpected diagnostic message: got %q, want %q", got, want)
|
||||
}
|
||||
|
||||
if got, want := actual.Hint, "Move the declaration before this use."; got != want {
|
||||
t.Fatalf("unexpected diagnostic hint: got %q, want %q", got, want)
|
||||
}
|
||||
|
||||
if got, want := len(actual.Spans), 2; got != want {
|
||||
t.Fatalf("expected %d spans, got %d", want, got)
|
||||
}
|
||||
|
||||
if got, want := actual.Spans[0].Label, "used before declaration"; got != want {
|
||||
t.Fatalf("unexpected main span label: got %q, want %q", got, want)
|
||||
}
|
||||
|
||||
if got, want := actual.Spans[1].Label, "declared later"; got != want {
|
||||
t.Fatalf("unexpected secondary span label: got %q, want %q", got, want)
|
||||
}
|
||||
|
||||
line, _ := actual.Source.LocationAt(actual.Spans[1].Span)
|
||||
if line != declarationLine {
|
||||
t.Fatalf("unexpected declaration span line: got %d, want %d", line, declarationLine)
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
pkgdiagnostics "github.com/MontFerret/ferret/v2/pkg/diagnostics"
|
||||
"github.com/MontFerret/ferret/v2/pkg/runtime"
|
||||
"github.com/MontFerret/ferret/v2/pkg/source"
|
||||
"github.com/MontFerret/ferret/v2/pkg/stdlib"
|
||||
"github.com/MontFerret/ferret/v2/pkg/vm"
|
||||
"github.com/MontFerret/ferret/v2/test/spec"
|
||||
. "github.com/MontFerret/ferret/v2/test/spec/exec"
|
||||
@@ -94,6 +95,92 @@ RETURN Outer()
|
||||
})
|
||||
}
|
||||
|
||||
func TestWaitForTimeoutFailureFormatsSourceSnippet(t *testing.T) {
|
||||
const query = `RETURN WAITFOR EXISTS []
|
||||
TIMEOUT 1ms
|
||||
EVERY 1ms
|
||||
ON TIMEOUT FAIL`
|
||||
|
||||
for _, level := range []compiler.OptimizationLevel{compiler.O0, compiler.O1} {
|
||||
t.Run(fmt.Sprintf("O%d", level), func(t *testing.T) {
|
||||
program, err := compiler.New(compiler.WithOptimizationLevel(level)).Compile(source.New("wait_timeout_fail.fql", query))
|
||||
if err != nil {
|
||||
t.Fatalf("compile failed: %v", err)
|
||||
}
|
||||
|
||||
instance, err := vm.New(program)
|
||||
if err != nil {
|
||||
t.Fatalf("vm init failed: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if closeErr := instance.Close(); closeErr != nil {
|
||||
t.Fatalf("vm close failed: %v", closeErr)
|
||||
}
|
||||
}()
|
||||
|
||||
env, err := vm.NewEnvironment([]vm.EnvironmentOption{
|
||||
vm.WithNamespace(stdlib.New()),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("environment init failed: %v", err)
|
||||
}
|
||||
|
||||
_, err = instance.Run(context.Background(), env)
|
||||
if err == nil {
|
||||
t.Fatal("expected runtime error")
|
||||
}
|
||||
|
||||
var runtimeErr *vm.RuntimeError
|
||||
if !errors.As(err, &runtimeErr) {
|
||||
t.Fatalf("expected runtime error, got %T", err)
|
||||
}
|
||||
|
||||
if got, want := runtimeErr.Message, "operation timed out"; got != want {
|
||||
t.Fatalf("unexpected runtime error message: got %q, want %q", got, want)
|
||||
}
|
||||
|
||||
mainSpanFound := false
|
||||
for _, span := range runtimeErr.Spans {
|
||||
if !span.Main {
|
||||
continue
|
||||
}
|
||||
|
||||
mainSpanFound = true
|
||||
|
||||
if span.Span.Start < 0 || span.Span.End > len(query) || span.Span.End <= span.Span.Start {
|
||||
t.Fatalf("unexpected main span bounds: %+v", span.Span)
|
||||
}
|
||||
|
||||
if got, want := query[span.Span.Start:span.Span.End], "ON TIMEOUT FAIL"; got != want {
|
||||
t.Fatalf("unexpected main span fragment: got %q, want %q", got, want)
|
||||
}
|
||||
|
||||
if got, want := span.Label, "operation timed out here"; got != want {
|
||||
t.Fatalf("unexpected main span label: got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
if !mainSpanFound {
|
||||
t.Fatal("expected a main error span")
|
||||
}
|
||||
|
||||
formatted := runtimeErr.Format()
|
||||
for _, needle := range []string{
|
||||
"UncaughtError: operation timed out",
|
||||
" --> wait_timeout_fail.fql:4:5",
|
||||
"3 | EVERY 1ms",
|
||||
"4 | ON TIMEOUT FAIL",
|
||||
"^^^^^^^^^^^^^^^ operation timed out here",
|
||||
"Caused by: operation timed out",
|
||||
} {
|
||||
if !strings.Contains(formatted, needle) {
|
||||
t.Fatalf("expected formatted runtime error to contain %q, got:\n%s", needle, formatted)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntimeErrorFormatsMissingParamWithParamSpan(t *testing.T) {
|
||||
const query = `RETURN @foo`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user