mirror of
https://github.com/MontFerret/ferret.git
synced 2026-06-20 01:17:53 +02:00
feat: enhance binary operation diagnostics with detailed operand spans
This commit is contained in:
@@ -63,7 +63,7 @@ func (c *BindingCompiler) compilePathAssignmentStatement(stmt *fql.AssignmentSta
|
||||
return bytecode.NoopOperand
|
||||
}
|
||||
|
||||
value = emitBinaryOperation(c.ctx, c.facts, stmt, op, current, right)
|
||||
value = emitBinaryOperation(c.ctx, c.facts, assignmentOperationSpans(stmt), op, current, right)
|
||||
}
|
||||
|
||||
if value == bytecode.NoopOperand {
|
||||
|
||||
@@ -188,7 +188,7 @@ func (c *BindingCompiler) CompileAssignmentStatement(ctx fql.IAssignmentStatemen
|
||||
|
||||
left := c.snapshotBindingValue(binding)
|
||||
right := c.exprs.Compile(stmt.Expression())
|
||||
src = emitBinaryOperation(c.ctx, c.facts, stmt, op, left, right)
|
||||
src = emitBinaryOperation(c.ctx, c.facts, assignmentOperationSpans(stmt), op, left, right)
|
||||
}
|
||||
|
||||
srcType := c.facts.OperandType(src)
|
||||
|
||||
@@ -40,6 +40,13 @@ type (
|
||||
regexp bool
|
||||
}
|
||||
|
||||
binaryOperationSpans struct {
|
||||
expression source.Span
|
||||
operator source.Span
|
||||
leftOperand source.Span
|
||||
rightOperand source.Span
|
||||
}
|
||||
|
||||
matchResultGroup struct {
|
||||
result fql.IExpressionContext
|
||||
arms []int
|
||||
@@ -150,7 +157,13 @@ func (c *ExprCompiler) CompileIncDec(token antlr.Token, target bytecode.Operand)
|
||||
}
|
||||
|
||||
operator := token.GetText()
|
||||
if !validateKnownNumericOperands(c.ctx, c.facts, parserd.SpanFromToken(token), operator, target) {
|
||||
if !validateKnownNumericOperands(
|
||||
c.ctx,
|
||||
c.facts,
|
||||
parserd.SpanFromToken(token),
|
||||
operator,
|
||||
numericOperandDiagnostic{operand: target},
|
||||
) {
|
||||
return bytecode.NoopOperand
|
||||
}
|
||||
|
||||
@@ -189,7 +202,17 @@ func (c *ExprCompiler) compileUnary(ctx fql.IUnaryOperatorContext, parent fql.IE
|
||||
span = parserd.SpanFromRuleContext(prc)
|
||||
}
|
||||
|
||||
if op != bytecode.OpNot && !validateKnownNumericOperands(c.ctx, c.facts, span, ctx.GetText(), src) {
|
||||
if op != bytecode.OpNot && !validateKnownNumericOperands(
|
||||
c.ctx,
|
||||
c.facts,
|
||||
parserd.SpanFromRuleContext(ctx),
|
||||
ctx.GetText(),
|
||||
numericOperandDiagnostic{
|
||||
operand: src,
|
||||
span: parserd.SpanFromRuleContext(parent.GetRight()),
|
||||
label: "operand",
|
||||
},
|
||||
) {
|
||||
return bytecode.NoopOperand
|
||||
}
|
||||
|
||||
|
||||
@@ -263,9 +263,7 @@ func (c *ExprCompiler) compileBinaryAtom(ctx fql.IExpressionAtomContext, op atom
|
||||
}
|
||||
|
||||
func (c *ExprCompiler) emitBinaryAtomOperation(ctx fql.IExpressionAtomContext, op atomBinaryOperator, left, right bytecode.Operand) bytecode.Operand {
|
||||
prc, _ := ctx.(antlr.ParserRuleContext)
|
||||
|
||||
return emitBinaryOperation(c.ctx, c.facts, prc, op, left, right)
|
||||
return emitBinaryOperation(c.ctx, c.facts, binaryAtomOperationSpans(ctx), op, left, right)
|
||||
}
|
||||
|
||||
func (c *ExprCompiler) validateRegexpOperand(ctx fql.IExpressionAtomContext) {
|
||||
|
||||
@@ -150,23 +150,30 @@ func resolveArithmeticBinaryOperator(operator string) (atomBinaryOperator, bool)
|
||||
}
|
||||
}
|
||||
|
||||
func emitBinaryOperation(ctx *CompilationSession, facts *TypeFacts, prc antlr.ParserRuleContext, op atomBinaryOperator, left, right bytecode.Operand) bytecode.Operand {
|
||||
type numericOperandDiagnostic struct {
|
||||
operand bytecode.Operand
|
||||
span source.Span
|
||||
label string
|
||||
}
|
||||
|
||||
func emitBinaryOperation(ctx *CompilationSession, facts *TypeFacts, spans binaryOperationSpans, op atomBinaryOperator, left, right bytecode.Operand) bytecode.Operand {
|
||||
if ctx == nil {
|
||||
return bytecode.NoopOperand
|
||||
}
|
||||
|
||||
span := source.Span{Start: -1, End: -1}
|
||||
|
||||
if prc != nil {
|
||||
span = parserd.SpanFromRuleContext(prc)
|
||||
}
|
||||
|
||||
if isStrictNumericBinaryOpcode(op.opcode) && !validateKnownNumericOperands(ctx, facts, span, op.text, left, right) {
|
||||
if isStrictNumericBinaryOpcode(op.opcode) && !validateKnownNumericOperands(
|
||||
ctx,
|
||||
facts,
|
||||
spans.operator,
|
||||
op.text,
|
||||
numericOperandDiagnostic{operand: left, span: spans.leftOperand, label: "left operand"},
|
||||
numericOperandDiagnostic{operand: right, span: spans.rightOperand, label: "right operand"},
|
||||
) {
|
||||
return bytecode.NoopOperand
|
||||
}
|
||||
|
||||
dst := ctx.Function.Registers.Allocate()
|
||||
ctx.Program.Emitter.WithSpan(span, func() {
|
||||
ctx.Program.Emitter.WithSpan(spans.expression, func() {
|
||||
ctx.Program.Emitter.EmitABC(op.opcode, dst, left, right)
|
||||
|
||||
if op.negated {
|
||||
@@ -197,26 +204,42 @@ func isStrictNumericBinaryOpcode(op bytecode.Opcode) bool {
|
||||
func validateKnownNumericOperands(
|
||||
ctx *CompilationSession,
|
||||
facts *TypeFacts,
|
||||
span source.Span,
|
||||
operatorSpan source.Span,
|
||||
operator string,
|
||||
operands ...bytecode.Operand,
|
||||
operands ...numericOperandDiagnostic,
|
||||
) bool {
|
||||
spans := []pkgdiagnostics.ErrorSpan{
|
||||
pkgdiagnostics.NewMainErrorSpan(operatorSpan, ""),
|
||||
}
|
||||
invalid := false
|
||||
|
||||
for _, operand := range operands {
|
||||
if knownNumericOperandTypeAllowed(facts.OperandType(operand)) {
|
||||
typ := facts.OperandType(operand.operand)
|
||||
if knownNumericOperandTypeAllowed(typ) {
|
||||
continue
|
||||
}
|
||||
|
||||
ctx.Program.Errors.Add(&pkgdiagnostics.Diagnostic{
|
||||
Kind: parserd.SemanticError,
|
||||
Message: fmt.Sprintf("Operator '%s' requires numeric operands", operator),
|
||||
Hint: "Use Int or Float values with this operator.",
|
||||
Spans: []pkgdiagnostics.ErrorSpan{pkgdiagnostics.NewMainErrorSpan(span, "")},
|
||||
})
|
||||
|
||||
return false
|
||||
invalid = true
|
||||
if operand.label != "" && validDiagnosticSpan(operand.span) {
|
||||
spans = append(spans, pkgdiagnostics.NewSecondaryErrorSpan(
|
||||
operand.span,
|
||||
fmt.Sprintf("%s is %s", operand.label, numericDiagnosticTypeName(typ)),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
if !invalid {
|
||||
return true
|
||||
}
|
||||
|
||||
ctx.Program.Errors.Add(&pkgdiagnostics.Diagnostic{
|
||||
Kind: parserd.SemanticError,
|
||||
Message: fmt.Sprintf("Operator '%s' requires numeric operands", operator),
|
||||
Hint: "Use Int or Float values with this operator.",
|
||||
Spans: spans,
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func knownNumericOperandTypeAllowed(typ core.ValueType) bool {
|
||||
@@ -227,3 +250,71 @@ func knownNumericOperandTypeAllowed(typ core.ValueType) bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func binaryAtomOperationSpans(ctx fql.IExpressionAtomContext) binaryOperationSpans {
|
||||
spans := binaryOperationSpans{
|
||||
expression: spanFromParserRuleContext(ctx),
|
||||
leftOperand: spanFromParserRuleContext(ctx.GetLeft()),
|
||||
rightOperand: spanFromParserRuleContext(ctx.GetRight()),
|
||||
}
|
||||
|
||||
if op := ctx.MultiplicativeOperator(); op != nil {
|
||||
spans.operator = parserd.SpanFromRuleContext(op)
|
||||
} else if op := ctx.AdditiveOperator(); op != nil {
|
||||
spans.operator = parserd.SpanFromRuleContext(op)
|
||||
}
|
||||
|
||||
return spans
|
||||
}
|
||||
|
||||
func assignmentOperationSpans(ctx *fql.AssignmentStatementContext) binaryOperationSpans {
|
||||
if ctx == nil {
|
||||
return binaryOperationSpans{}
|
||||
}
|
||||
|
||||
return binaryOperationSpans{
|
||||
expression: parserd.SpanFromRuleContext(ctx),
|
||||
operator: spanFromParserRuleContext(ctx.AssignmentOperator()),
|
||||
leftOperand: spanFromParserRuleContext(ctx.AssignmentTarget()),
|
||||
rightOperand: spanFromParserRuleContext(ctx.Expression()),
|
||||
}
|
||||
}
|
||||
|
||||
func spanFromParserRuleContext(ctx antlr.ParserRuleContext) source.Span {
|
||||
if ctx == nil {
|
||||
return source.Span{Start: -1, End: -1}
|
||||
}
|
||||
|
||||
return parserd.SpanFromRuleContext(ctx)
|
||||
}
|
||||
|
||||
func validDiagnosticSpan(span source.Span) bool {
|
||||
return span.Start >= 0 && span.End > span.Start
|
||||
}
|
||||
|
||||
func numericDiagnosticTypeName(typ core.ValueType) string {
|
||||
switch typ {
|
||||
case core.TypeNone:
|
||||
return "None"
|
||||
case core.TypeInt:
|
||||
return "Int"
|
||||
case core.TypeFloat:
|
||||
return "Float"
|
||||
case core.TypeString:
|
||||
return "String"
|
||||
case core.TypeBool:
|
||||
return "Bool"
|
||||
case core.TypeArray:
|
||||
return "Array"
|
||||
case core.TypeObject:
|
||||
return "Object"
|
||||
case core.TypeList:
|
||||
return "List"
|
||||
case core.TypeMap:
|
||||
return "Map"
|
||||
case core.TypeAny:
|
||||
return "Any"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package compiler_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/MontFerret/ferret/v2/pkg/compiler"
|
||||
"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"
|
||||
)
|
||||
@@ -26,3 +30,119 @@ func TestMathOperatorsRejectKnownNonNumericOperands(t *testing.T) {
|
||||
Failure(`RETURN +"3"`, expected("+"), "string unary positive"),
|
||||
})
|
||||
}
|
||||
|
||||
func TestMathOperatorDiagnosticSpansMultiline(t *testing.T) {
|
||||
src := `RETURN [
|
||||
120,
|
||||
45,
|
||||
300
|
||||
] * 2`
|
||||
diag := compileMathOperatorDiagnostic(t, "arithmetic.fql", src)
|
||||
|
||||
assertMathOperatorDiagnostic(t, diag, "*", 2)
|
||||
assertMathOperatorSpan(t, diag.Spans[0], strings.Index(src, "*"), strings.Index(src, "*")+1, "", true)
|
||||
assertMathOperatorSpan(t, diag.Spans[1], strings.Index(src, "["), strings.Index(src, "]")+1, "left operand is Array", false)
|
||||
|
||||
expected := `SemanticError: Operator '*' requires numeric operands
|
||||
--> arithmetic.fql:1:8
|
||||
|
|
||||
1 | RETURN [
|
||||
| ^ left operand is Array
|
||||
2 | 120,
|
||||
--> arithmetic.fql:5:3
|
||||
|
|
||||
4 | 300
|
||||
5 | ] * 2
|
||||
| ^
|
||||
Hint: Use Int or Float values with this operator.
|
||||
`
|
||||
if actual := diag.Format(); actual != expected {
|
||||
t.Fatalf("unexpected formatted diagnostic:\n%s\nDiff expected:\n%s", actual, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMathOperatorDiagnosticIdentifiesInvalidOperands(t *testing.T) {
|
||||
t.Run("right operand", func(t *testing.T) {
|
||||
src := `RETURN 2 * "x"`
|
||||
diag := compileMathOperatorDiagnostic(t, "right_operand.fql", src)
|
||||
|
||||
assertMathOperatorDiagnostic(t, diag, "*", 2)
|
||||
assertMathOperatorSpan(t, diag.Spans[0], strings.Index(src, "*"), strings.Index(src, "*")+1, "", true)
|
||||
assertMathOperatorSpan(t, diag.Spans[1], strings.Index(src, `"x"`), strings.Index(src, `"x"`)+3, "right operand is String", false)
|
||||
})
|
||||
|
||||
t.Run("both operands", func(t *testing.T) {
|
||||
src := `RETURN TRUE * {}`
|
||||
diag := compileMathOperatorDiagnostic(t, "both_operands.fql", src)
|
||||
|
||||
assertMathOperatorDiagnostic(t, diag, "*", 3)
|
||||
assertMathOperatorSpan(t, diag.Spans[0], strings.Index(src, "*"), strings.Index(src, "*")+1, "", true)
|
||||
assertMathOperatorSpan(t, diag.Spans[1], strings.Index(src, "TRUE"), strings.Index(src, "TRUE")+4, "left operand is Bool", false)
|
||||
assertMathOperatorSpan(t, diag.Spans[2], strings.Index(src, "{}"), strings.Index(src, "{}")+2, "right operand is Object", false)
|
||||
})
|
||||
|
||||
t.Run("unary operand", func(t *testing.T) {
|
||||
src := `RETURN -"x"`
|
||||
diag := compileMathOperatorDiagnostic(t, "unary_operand.fql", src)
|
||||
|
||||
assertMathOperatorDiagnostic(t, diag, "-", 2)
|
||||
assertMathOperatorSpan(t, diag.Spans[0], strings.Index(src, "-"), strings.Index(src, "-")+1, "", true)
|
||||
assertMathOperatorSpan(t, diag.Spans[1], strings.Index(src, `"x"`), strings.Index(src, `"x"`)+3, "operand is String", false)
|
||||
})
|
||||
|
||||
t.Run("augmented assignment right operand", func(t *testing.T) {
|
||||
src := "VAR total = 2\ntotal *= \"x\"\nRETURN total"
|
||||
diag := compileMathOperatorDiagnostic(t, "assignment_operand.fql", src)
|
||||
|
||||
assertMathOperatorDiagnostic(t, diag, "*=", 2)
|
||||
assertMathOperatorSpan(t, diag.Spans[0], strings.Index(src, "*="), strings.Index(src, "*=")+2, "", true)
|
||||
assertMathOperatorSpan(t, diag.Spans[1], strings.Index(src, `"x"`), strings.Index(src, `"x"`)+3, "right operand is String", false)
|
||||
})
|
||||
}
|
||||
|
||||
func compileMathOperatorDiagnostic(t *testing.T, name, src string) *diagnostics.Diagnostic {
|
||||
t.Helper()
|
||||
|
||||
_, err := compiler.New().Compile(source.New(name, src))
|
||||
if err == nil {
|
||||
t.Fatal("expected compilation error")
|
||||
}
|
||||
|
||||
diag := firstCompilationError(err)
|
||||
if diag == nil {
|
||||
t.Fatalf("expected diagnostic, got %T", err)
|
||||
}
|
||||
|
||||
return diag
|
||||
}
|
||||
|
||||
func assertMathOperatorDiagnostic(t *testing.T, diag *diagnostics.Diagnostic, operator string, spanCount int) {
|
||||
t.Helper()
|
||||
|
||||
if diag.Kind != parserd.SemanticError {
|
||||
t.Fatalf("unexpected diagnostic kind: got %s want %s", diag.Kind, parserd.SemanticError)
|
||||
}
|
||||
if diag.Message != "Operator '"+operator+"' requires numeric operands" {
|
||||
t.Fatalf("unexpected diagnostic message: %q", diag.Message)
|
||||
}
|
||||
if diag.Hint != "Use Int or Float values with this operator." {
|
||||
t.Fatalf("unexpected diagnostic hint: %q", diag.Hint)
|
||||
}
|
||||
if len(diag.Spans) != spanCount {
|
||||
t.Fatalf("unexpected span count: got %d want %d", len(diag.Spans), spanCount)
|
||||
}
|
||||
}
|
||||
|
||||
func assertMathOperatorSpan(t *testing.T, actual diagnostics.ErrorSpan, start, end int, label string, main bool) {
|
||||
t.Helper()
|
||||
|
||||
if actual.Span.Start != start || actual.Span.End != end {
|
||||
t.Fatalf("unexpected span: got [%d,%d) want [%d,%d)", actual.Span.Start, actual.Span.End, start, end)
|
||||
}
|
||||
if actual.Label != label {
|
||||
t.Fatalf("unexpected span label: got %q want %q", actual.Label, label)
|
||||
}
|
||||
if actual.Main != main {
|
||||
t.Fatalf("unexpected main flag: got %t want %t", actual.Main, main)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user