1
0
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:
Tim Voronov
2026-06-02 11:31:21 -04:00
committed by GitHub
parent 12a344fb39
commit fb867c4957
20 changed files with 1612 additions and 20 deletions
+1 -1
View File
@@ -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
}
+1 -1
View File
@@ -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
}
+1
View File
@@ -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)
}
+1 -1
View File
@@ -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
}
+4
View File
@@ -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)
+1 -1
View File
@@ -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)
+17 -9
View File
@@ -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)
})
}
}
+24
View File
@@ -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
+10 -6
View File
@@ -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) {
+4
View File
@@ -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)
+1
View File
@@ -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 {
+20
View File
@@ -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`