diff --git a/README.md b/README.md index 6cf6477..4017cd9 100644 --- a/README.md +++ b/README.md @@ -87,20 +87,29 @@ Superpower.ParseException: Syntax error (line 1, column 21): Unexpected left bra at Superpower.Tokenizer`1.Tokenize(String source) at PromQL.Parser.Parser.ParseExpression(String input, Tokenizer tokenizer) at UserQuery.Main() in C:\Users\james.luck\AppData\Local\Temp\LINQPad6\_aryncqeb\dwfjew\LINQPadQuery:line [[ -``` +``` -**Only syntax is validated, semantic validation is not applied**. For example, the following expressions -are considered valid by this library: +### Type checking +Syntax is not only validated but expressions can also be typed checked via `CheckType`, e.g: +``` +Parser.ParseExpression("1 + sum_over_time(some_metric[1h])").CheckType().Dump(); +``` +Returns: +``` +ValueType.Vector ``` -# Invalid number of function arguments -time(1, 2, 3, 4) - -# Invalid number and types of aggregation function arguments -sum(1.0, "a string") -# Binary operations not defined for strings -"a" + "b" +Expressions that violate the PromQL type system will throw an exception, e.g: +``` +Parser.ParseExpression("'a' + 'b'").CheckType(); +``` +Throws: +``` +Unexpected type 'string' was provided, expected one of 'scalar', 'instant vector': 0 (line 1, column 1) + at PromQL.Parser.TypeChecker.ExpectTypes(Expr expr, ValueType[] expected) + at PromQL.Parser.TypeChecker.CheckType(Expr expr) + at UserQuery.Main(), line 1 ``` ### Modifying PromQL expressions @@ -161,7 +170,7 @@ This library aims to parse 99% of the PromQL language and has [extensive test co - Hexadecimal/ octal string escaping - Hexadecimal number representation - [The `@` modifier](https://prometheus.io/docs/prometheus/latest/querying/basics/#modifier) -- Operator associativity (all operators are parsed with the same associativity) +- Exponential operator associativity (is parsed with left associativity) If any of these features are important to you, please create an issue or submit a pull request. diff --git a/src/PromQL.Parser/Ast.cs b/src/PromQL.Parser/Ast.cs index 385f70e..3798b29 100644 --- a/src/PromQL.Parser/Ast.cs +++ b/src/PromQL.Parser/Ast.cs @@ -2,6 +2,8 @@ using System.Collections.Immutable; using System.Text; using ExhaustiveMatching; +using Superpower.Model; +using Superpower.Parsers; namespace PromQL.Parser.Ast { @@ -11,6 +13,7 @@ namespace PromQL.Parser.Ast public interface IPromQlNode { void Accept(IVisitor visitor); + TextSpan? Span { get; } } /// @@ -29,34 +32,39 @@ public interface IPromQlNode typeof(UnaryExpr), typeof(VectorSelector) )] - public interface Expr : IPromQlNode {} + public interface Expr : IPromQlNode + { + ValueType Type { get; } + } /// /// Represents an aggregation operation on a Vector. /// - /// The used aggregation operation. + /// The used aggregation operation. /// The Vector expression over which is aggregated. /// Parameter used by some aggregators. /// The labels by which to group the Vector. /// Whether to drop the given labels rather than keep them. - public record AggregateExpr(string OperatorName, Expr Expr, Expr? Param, - ImmutableArray GroupingLabels, bool Without) : Expr + public record AggregateExpr(AggregateOperator Operator, Expr Expr, Expr? Param, + ImmutableArray GroupingLabels, bool Without, TextSpan? Span = null) : Expr { - public AggregateExpr(string operatorName, Expr expr) - : this (operatorName, expr, null, ImmutableArray.Empty, false) + public AggregateExpr(AggregateOperator @operator, Expr expr) + : this (@operator, expr, null, ImmutableArray.Empty, false) { } - public AggregateExpr(string operatorName, Expr expr, Expr param, bool without = false, params string[] groupingLabels) - : this (operatorName, expr, param, groupingLabels.ToImmutableArray(), without) + public AggregateExpr(AggregateOperator @operator, Expr expr, Expr param, bool without = false, params string[] groupingLabels) + : this (@operator, expr, param, groupingLabels.ToImmutableArray(), without) { } - public string OperatorName { get; set;} = OperatorName; + public AggregateOperator Operator { get; set;} = Operator; public Expr Expr { get; set;} = Expr; public Expr? Param { get; set;} = Param; public ImmutableArray GroupingLabels { get; set;} = GroupingLabels; public bool Without { get; set;} = Without; + + public ValueType Type => ValueType.Vector; public void Accept(IVisitor visitor) => visitor.Visit(this); } @@ -69,13 +77,24 @@ public AggregateExpr(string operatorName, Expr expr, Expr param, bool without = /// The operation of the expression /// The matching behavior for the operation to be applied if both operands are Vectors. public record BinaryExpr(Expr LeftHandSide, Expr RightHandSide, Operators.Binary Operator, - VectorMatching? VectorMatching = null) : Expr + VectorMatching? VectorMatching = null, TextSpan? Span = null) : Expr { public Expr LeftHandSide { get; set; } = LeftHandSide; public Expr RightHandSide { get; set; } = RightHandSide; public Operators.Binary Operator { get; set; } = Operator; public VectorMatching? VectorMatching { get; set; } = VectorMatching; public void Accept(IVisitor visitor) => visitor.Visit(this); + + public ValueType Type + { + get + { + if (RightHandSide.Type == ValueType.Scalar && LeftHandSide.Type == ValueType.Scalar) + return ValueType.Scalar; + + return ValueType.Vector; + } + } } /// @@ -87,7 +106,7 @@ public record BinaryExpr(Expr LeftHandSide, Expr RightHandSide, Operators.Binary /// Contains additional labels that should be included in the result from the side with the lower cardinality. /// If a comparison operator, return 0/1 rather than filtering. public record VectorMatching(Operators.VectorMatchCardinality MatchCardinality, ImmutableArray MatchingLabels, - bool On, ImmutableArray Include, bool ReturnBool) : IPromQlNode + bool On, ImmutableArray Include, bool ReturnBool, TextSpan? Span = null) : IPromQlNode { public static Operators.VectorMatchCardinality DefaultMatchCardinality { get; } = Operators.VectorMatchCardinality.OneToOne; @@ -104,91 +123,104 @@ public VectorMatching(bool returnBool) : this (DefaultMatchCardinality, Immutabl public bool On { get; set; } = On; public ImmutableArray Include { get; set; } = Include; public bool ReturnBool { get; set; } = ReturnBool; - + public void Accept(IVisitor visitor) => visitor.Visit(this); }; /// /// A function call. /// - /// The function that was called. + /// The function that was called. /// Arguments used in the call. - public record FunctionCall(string Identifier, ImmutableArray Args) : Expr + public record FunctionCall(Function Function, ImmutableArray Args, TextSpan? Span = null) : Expr { - public FunctionCall(string identifier, params Expr[] args) - : this (identifier, args.ToImmutableArray()) + public FunctionCall(Function function, params Expr[] args) + : this (function, args.ToImmutableArray()) { } - public string Identifier { get; set; } = Identifier; + public Function Function { get; set; } = Function; public ImmutableArray Args { get; set; } = Args; - - public void Accept(IVisitor visitor) => visitor.Visit(this); + public ValueType Type => Function.ReturnType; + + public void Accept(IVisitor visitor) => visitor.Visit(this); + protected virtual bool PrintMembers(StringBuilder builder) { - builder.AppendLine($"{nameof(Identifier)} = {Identifier}, "); + builder.AppendLine($"{nameof(Function)} = {Function.Name}, "); builder.Append($"{nameof(Args)} = "); Args.PrintArray(builder); - + return true; } } - public record ParenExpression(Expr Expr) : Expr + public record ParenExpression(Expr Expr, TextSpan? Span = null) : Expr { public Expr Expr { get; set; } = Expr; public void Accept(IVisitor visitor) => visitor.Visit(this); + public ValueType Type => Expr.Type; } - public record OffsetExpr(Expr Expr, Duration Duration) : Expr + public record OffsetExpr(Expr Expr, Duration Duration, TextSpan? Span = null) : Expr { public Expr Expr { get; set; } = Expr; public Duration Duration { get; set; } = Duration; public void Accept(IVisitor visitor) => visitor.Visit(this); + public ValueType Type => Expr.Type; } - public record MatrixSelector(VectorSelector Vector, Duration Duration) : Expr + public record MatrixSelector(VectorSelector Vector, Duration Duration, TextSpan? Span = null) : Expr { public VectorSelector Vector { get; set; } =Vector; public Duration Duration { get; set; } = Duration; public void Accept(IVisitor visitor) => visitor.Visit(this); + public ValueType Type => ValueType.Matrix; } - public record UnaryExpr(Operators.Unary Operator, Expr Expr) : Expr + public record UnaryExpr(Operators.Unary Operator, Expr Expr, TextSpan? Span = null) : Expr { public Operators.Unary Operator { get; set; } = Operator; public Expr Expr { get; set; } = Expr; public void Accept(IVisitor visitor) => visitor.Visit(this); + public ValueType Type => Expr.Type; } public record VectorSelector : Expr { - public VectorSelector(MetricIdentifier metricIdentifier) + public VectorSelector(MetricIdentifier metricIdentifier, TextSpan? span = null) { MetricIdentifier = metricIdentifier; + Span = span; } - public VectorSelector(LabelMatchers labelMatchers) + public VectorSelector(LabelMatchers labelMatchers, TextSpan? span = null) { LabelMatchers = labelMatchers; + Span = span; } - public VectorSelector(MetricIdentifier metricIdentifier, LabelMatchers labelMatchers) + public VectorSelector(MetricIdentifier metricIdentifier, LabelMatchers labelMatchers, TextSpan? span = null) { + MetricIdentifier = metricIdentifier; LabelMatchers = labelMatchers; + Span = span; } public MetricIdentifier? MetricIdentifier { get; set; } public LabelMatchers? LabelMatchers { get; set; } + public TextSpan? Span { get; } + public ValueType Type => ValueType.Vector; + public void Accept(IVisitor visitor) => visitor.Visit(this); } - public record LabelMatchers(ImmutableArray Matchers) : IPromQlNode + public record LabelMatchers(ImmutableArray Matchers, TextSpan? Span = null) : IPromQlNode { - protected virtual bool PrintMembers(StringBuilder builder) + protected virtual bool PrintMembers(System.Text.StringBuilder builder) { builder.Append($"{nameof(Matchers)} = "); Matchers.PrintArray(builder); @@ -201,43 +233,46 @@ protected virtual bool PrintMembers(StringBuilder builder) public void Accept(IVisitor visitor) => visitor.Visit(this); } - public record LabelMatcher(string LabelName, Operators.LabelMatch Operator, StringLiteral Value) : IPromQlNode + public record LabelMatcher(string LabelName, Operators.LabelMatch Operator, StringLiteral Value, TextSpan? Span = null) : IPromQlNode { public void Accept(IVisitor visitor) => visitor.Visit(this); } - public record MetricIdentifier(string Value) : IPromQlNode + public record MetricIdentifier(string Value, TextSpan? Span = null) : IPromQlNode { public void Accept(IVisitor visitor) => visitor.Visit(this); } - public record NumberLiteral(double Value) : Expr + public record NumberLiteral(double Value, TextSpan? Span = null) : Expr { public void Accept(IVisitor visitor) => visitor.Visit(this); + public ValueType Type => ValueType.Scalar; } - public record Duration(TimeSpan Value) : IPromQlNode + public record Duration(TimeSpan Value, TextSpan? Span = null) : IPromQlNode { public void Accept(IVisitor visitor) => visitor.Visit(this); } - public record StringLiteral(char Quote, string Value) : Expr + public record StringLiteral(char Quote, string Value, TextSpan? Span = null) : Expr { public void Accept(IVisitor visitor) => visitor.Visit(this); + public ValueType Type => ValueType.String; } - public record SubqueryExpr(Expr Expr, Duration Range, Duration? Step = null) : Expr + public record SubqueryExpr(Expr Expr, Duration Range, Duration? Step = null, TextSpan? Span = null) : Expr { public Expr Expr { get; set; } = Expr; public Duration Range { get; set; } = Range; public Duration? Step { get; set; } = Step; + public ValueType Type => ValueType.Matrix; public void Accept(IVisitor visitor) => visitor.Visit(this); } internal static class Extensions { - internal static void PrintArray(this ImmutableArray arr, StringBuilder sb) + internal static void PrintArray(this ImmutableArray arr, System.Text.StringBuilder sb) where T : notnull { sb.Append("[ "); diff --git a/src/PromQL.Parser/Functions.cs b/src/PromQL.Parser/Functions.cs index f64c9ca..de8c0d1 100644 --- a/src/PromQL.Parser/Functions.cs +++ b/src/PromQL.Parser/Functions.cs @@ -11,74 +11,86 @@ public static class Functions /// Primarily taken from https://github.com/prometheus/prometheus/blob/main/web/ui/module/codemirror-promql/src/grammar/promql.grammar#L121-L188. /// More authoritative source would be https://github.com/prometheus/prometheus/blob/7471208b5c8ff6b65b644adedf7eb964da3d50ae/promql/parser/functions.go. /// - public static ImmutableHashSet Names = new[] + public static ImmutableDictionary Map { get; set; } = new[] { - "absent_over_time", - "absent", - "abs", - "acos", - "acosh", - "asin", - "asinh", - "atan", - "atanh", - "avg_over_time", - "ceil", - "changes", - "clamp", - "clamp_max", - "clamp_min", - "cos", - "cosh", - "count_over_time", - "days_in_month", - "day_of_month", - "day_of_week", - "deg", - "delta", - "deriv", - "exp", - "floor", - "histogram_quantile", - "holt_winters", - "hour", - "idelta", - "increase", - "irate", - "label_replace", - "label_join", - "last_over_time", - "ln", - "log_10", - "log_2", - "max_over_time", - "min_over_time", - "minute", - "month", - "pi", - "predict_linear", - "present_over_time", - "quantile_over_time", - "rad", - "rate", - "resets", - "round", - "scalar", - "sgn", - "sin", - "sinh", - "sort", - "sort_desc", - "sqrt", - "stddev_over_time", - "stdvar_over_time", - "sum_over_time", - "tan", - "tanh", - "timestamp", - "time", - "vector", - "year" - }.ToImmutableHashSet(); + new Function("absent", ValueType.Vector, ValueType.Vector), + new Function("absent_over_time", ValueType.Vector, ValueType.Matrix), + new Function("abs", ValueType.Vector, ValueType.Vector), + new Function("acos", ValueType.Vector, ValueType.Vector), + new Function("acosh", ValueType.Vector, ValueType.Vector), + new Function("asin", ValueType.Vector, ValueType.Vector), + new Function("asinh", ValueType.Vector, ValueType.Vector), + new Function("atan", ValueType.Vector, ValueType.Vector), + new Function("atanh", ValueType.Vector, ValueType.Vector), + new Function("avg_over_time", ValueType.Vector, ValueType.Matrix), + new Function("ceil", ValueType.Vector, ValueType.Vector), + new Function("changes", ValueType.Vector, ValueType.Matrix), + new Function("clamp", ValueType.Vector, ValueType.Vector, ValueType.Scalar, ValueType.Scalar), + new Function("clamp_max", ValueType.Vector, ValueType.Vector, ValueType.Scalar), + new Function("clamp_min", ValueType.Vector, ValueType.Vector, ValueType.Scalar), + new Function("cos", ValueType.Vector, ValueType.Vector), + new Function("cosh", ValueType.Vector, ValueType.Vector), + new Function("count_over_time", ValueType.Vector, ValueType.Matrix), + new Function("days_in_month", ValueType.Vector, varadicModifier: 1 , ValueType.Vector), + new Function("day_of_month", ValueType.Vector, varadicModifier: 1, ValueType.Vector), + new Function("day_of_week", ValueType.Vector, varadicModifier: 1, ValueType.Vector), + new Function("deg", ValueType.Vector, ValueType.Vector), + new Function("delta", ValueType.Vector, ValueType.Matrix), + new Function("deriv", ValueType.Vector, ValueType.Matrix), + new Function("exp", ValueType.Vector, ValueType.Vector), + new Function("floor", ValueType.Vector, ValueType.Vector), + new Function("histogram_quantile", ValueType.Vector, ValueType.Scalar, ValueType.Vector), + new Function("holt_winters", ValueType.Vector, ValueType.Matrix, ValueType.Scalar, ValueType.Scalar), + new Function("hour", ValueType.Vector, varadicModifier: 1, ValueType.Vector), + new Function("idelta", ValueType.Vector, ValueType.Matrix), + new Function("increase", ValueType.Vector, ValueType.Matrix), + new Function("irate", ValueType.Vector, ValueType.Matrix), + new Function("label_replace", ValueType.Vector, ValueType.Vector, ValueType.String, ValueType.String, ValueType.String, ValueType.String), + new Function("label_join", ValueType.Vector, varadicModifier: 0, ValueType.Vector, ValueType.String, ValueType.String, ValueType.String), + new Function("last_over_time", ValueType.Vector, ValueType.Matrix), + new Function("ln", ValueType.Vector, ValueType.Vector), + new Function("log_10", ValueType.Vector, ValueType.Vector), + new Function("log_2", ValueType.Vector, ValueType.Vector), + new Function("max_over_time", ValueType.Vector, ValueType.Matrix), + new Function("min_over_time", ValueType.Vector, ValueType.Matrix), + new Function("minute", ValueType.Vector, varadicModifier: 1, ValueType.Vector), + new Function("month", ValueType.Vector, varadicModifier: 1, ValueType.Vector), + new Function("pi", ValueType.Scalar), + new Function("predict_linear", ValueType.Scalar, ValueType.Matrix, ValueType.Scalar), + new Function("present_over_time", ValueType.Vector, ValueType.Matrix), + new Function("quantile_over_time", ValueType.Vector, ValueType.Scalar, ValueType.Matrix), + new Function("rad", ValueType.Vector, ValueType.Vector), + new Function("rate", ValueType.Vector, ValueType.Matrix), + new Function("resets", ValueType.Vector, ValueType.Matrix), + new Function("round", ValueType.Vector, varadicModifier: 1, ValueType.Vector, ValueType.Scalar), + new Function("scalar", ValueType.Scalar, ValueType.Vector), + new Function("sgn", ValueType.Vector, ValueType.Vector), + new Function("sin", ValueType.Vector, ValueType.Vector), + new Function("sinh", ValueType.Vector, ValueType.Vector), + new Function("sort", ValueType.Vector, ValueType.Vector), + new Function("sort_desc", ValueType.Vector, ValueType.Vector), + new Function("sqrt", ValueType.Vector, ValueType.Vector), + new Function("stddev_over_time", ValueType.Vector, ValueType.Matrix), + new Function("stdvar_over_time", ValueType.Vector, ValueType.Matrix), + new Function("sum_over_time", ValueType.Vector, ValueType.Matrix), + new Function("tan", ValueType.Vector, ValueType.Vector), + new Function("tanh", ValueType.Vector, ValueType.Vector), + new Function("timestamp", ValueType.Vector, ValueType.Vector), + new Function("time", ValueType.Scalar), + new Function("vector", ValueType.Vector, ValueType.Scalar), + new Function("year", ValueType.Vector, varadicModifier: 1, ValueType.Vector) + }.ToImmutableDictionary(k => k.Name); + } + + public record Function(string Name, ValueType ReturnType, ImmutableArray ArgTypes, int? VariadicModifier = null) + { + public Function(string name, ValueType returnType, params ValueType[] argTypes) + : this(name, returnType, argTypes.ToImmutableArray(), null) { } + + public Function(string name, ValueType returnType, int varadicModifier, params ValueType[] argTypes) + : this(name, returnType, argTypes.ToImmutableArray(), varadicModifier) { } + + public bool IsVariadic => VariadicModifier != null; + public int MinArgCount => ArgTypes.Length - (VariadicModifier ?? 0); } } diff --git a/src/PromQL.Parser/Operators.cs b/src/PromQL.Parser/Operators.cs index 3bf058a..19bdae5 100644 --- a/src/PromQL.Parser/Operators.cs +++ b/src/PromQL.Parser/Operators.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Immutable; +using System.Linq; using ExhaustiveMatching; namespace PromQL.Parser @@ -16,26 +17,7 @@ public enum LabelMatch Regexp, NotRegexp } - - /// - /// Defines the set of all valid aggregator operators (e.g. sum, avg, etc.) - /// - public static ImmutableHashSet Aggregates = new [] - { - "sum", - "avg", - "count", - "min", - "max", - "group", - "stddev", - "stdvar", - "topk", - "bottomk", - "count_values", - "quantile", - }.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase); - + /// /// Describes the cardinality relationship of two Vectors in a binary operation. /// @@ -54,7 +36,7 @@ public enum VectorMatchCardinality /// OneToMany } - + public enum Binary { Pow, @@ -86,7 +68,64 @@ public enum Unary /// Sub } - + + /// + /// The set of binary operators that operate over sets (instant vectors) only. + /// + public static ImmutableHashSet BinarySetOperators { get; set; } = new [] + { + Binary.And, + Binary.Or, + Binary.Unless + }.ToImmutableHashSet(); + + /// + /// The set of binary operations that compare expressions. + /// + /// https://github.com/prometheus/prometheus/blob/f103acd5135b8bbe885b17a73dafc7bbb586319c/promql/parser/lex.go#L71 + public static ImmutableHashSet BinaryComparisonOperators { get; set; }= new[] + { + Binary.Gtr, + Binary.Gte, + Binary.Lss, + Binary.Lte, + Binary.Eql, + Binary.Neq + }.ToImmutableHashSet(); + + /// + /// Operators are ordered by highest -> lowest precedence. + /// + public static ImmutableArray> BinaryPrecedence { get; set; } = new[] + { + // TODO support right associativity for pow! + new[] { Binary.Pow }, + new[] { Binary.Mul, Binary.Div, Binary.Atan2, Binary.Mod }, + new[] { Binary.Add, Binary.Sub }, + new[] { Binary.Eql, Binary.Neq, Binary.Gtr, Binary.Gte, Binary.Lss, Binary.Lte }, + new[] { Binary.And, Binary.Unless }, + new[] { Binary.Or } + }.Select(x => x.ToImmutableHashSet()).ToImmutableArray(); + + /// + /// Defines the set of all valid aggregator operators (e.g. sum, avg, etc.) + /// + public static ImmutableDictionary Aggregates { get; set; } = new [] + { + new AggregateOperator("sum"), + new AggregateOperator("avg"), + new AggregateOperator("count"), + new AggregateOperator("min"), + new AggregateOperator("max"), + new AggregateOperator("group"), + new AggregateOperator("stddev"), + new AggregateOperator("stdvar"), + new AggregateOperator("topk", ValueType.Scalar), + new AggregateOperator("bottomk", ValueType.Scalar), + new AggregateOperator("count_values", ValueType.String), + new AggregateOperator("quantile", ValueType.Scalar) + }.ToImmutableDictionary(k => k.Name, v => v, StringComparer.OrdinalIgnoreCase); + public static string ToPromQl(this Operators.Binary op) => op switch { Operators.Binary.Add => "+", @@ -132,4 +171,6 @@ public enum Unary _ => throw ExhaustiveMatch.Failed(op) }; } + + public record AggregateOperator(string Name, ValueType? ParameterType = null); } diff --git a/src/PromQL.Parser/Parser.cs b/src/PromQL.Parser/Parser.cs index e6067a1..89ca44f 100644 --- a/src/PromQL.Parser/Parser.cs +++ b/src/PromQL.Parser/Parser.cs @@ -20,15 +20,15 @@ namespace PromQL.Parser /// public static class Parser { - public static TokenListParser UnaryOperator = - Token.EqualTo(PromToken.ADD).Select(_ => Operators.Unary.Add).Or( - Token.EqualTo(PromToken.SUB).Select(_ => Operators.Unary.Sub) + public static TokenListParser> UnaryOperator = + Token.EqualTo(PromToken.ADD).Select(t => Operators.Unary.Add.ToParsedValue(t.Span)).Or( + Token.EqualTo(PromToken.SUB).Select(t => Operators.Unary.Sub.ToParsedValue(t.Span)) ); public static TokenListParser UnaryExpr = from op in Parse.Ref(() => UnaryOperator) from expr in Parse.Ref(() => Expr) - select new UnaryExpr(op, expr); + select new UnaryExpr(op.Value, expr, op.Span.UntilEnd(expr.Span)); private static IEnumerable FindTokensMatching(Func predicate) => typeof(PromToken).GetMembers() .Select(enumMember => (enumMember, attr: enumMember.GetCustomAttributes(typeof(TokenAttribute), false).Cast().SingleOrDefault())) @@ -46,9 +46,9 @@ private static IEnumerable FindTokensMatching(Func MetricIdentifier = from id in Token.EqualTo(PromToken.METRIC_IDENTIFIER) .Or(Token.EqualTo(PromToken.IDENTIFIER)) - .Or(Token.EqualTo(PromToken.AGGREGATE_OP).Where(t => Operators.Aggregates.Contains(t.ToStringValue()), "aggregate_op")) + .Or(Token.EqualTo(PromToken.AGGREGATE_OP).Where(t => Operators.Aggregates.ContainsKey(t.ToStringValue()), "aggregate_op")) .Or(Token.Matching(t => AlphanumericOperatorTokens.Contains(t), "operator")) - select new MetricIdentifier(id.ToStringValue()); + select new MetricIdentifier(id.ToStringValue(), id.Span); public static TokenListParser LabelMatchers = from lb in Token.EqualTo(PromToken.LEFT_BRACE) @@ -63,38 +63,40 @@ from comma in Token.EqualTo(PromToken.COMMA).Optional() select new [] { matcherHead }.Concat(matcherTail) ).OptionalOrDefault(Array.Empty()) from rb in Token.EqualTo(PromToken.RIGHT_BRACE) - select new LabelMatchers(matchers.ToImmutableArray()); + select new LabelMatchers(matchers.ToImmutableArray(), lb.Span.UntilEnd(rb.Span)); public static TokenListParser VectorSelector = ( from m in MetricIdentifier from lm in LabelMatchers.AsNullable().OptionalOrDefault() - select new VectorSelector(m, lm) + select new VectorSelector(m, lm, lm != null ? m.Span!.Value.UntilEnd(lm.Span) : m.Span!) ).Or( from lm in LabelMatchers - select new VectorSelector(lm) + select new VectorSelector(lm, lm.Span) ); public static TokenListParser MatrixSelector = from vs in VectorSelector - from d in Parse.Ref(() => Duration).Between(Token.EqualTo(PromToken.LEFT_BRACKET), Token.EqualTo(PromToken.RIGHT_BRACKET)) - select new MatrixSelector(vs, d); + from lb in Token.EqualTo(PromToken.LEFT_BRACKET) + from d in Parse.Ref(() => Duration) + from rb in Token.EqualTo(PromToken.RIGHT_BRACKET) + select new MatrixSelector(vs, d, vs.Span!.Value.UntilEnd(rb.Span)); // TODO see https://github.com/prometheus/prometheus/blob/7471208b5c8ff6b65b644adedf7eb964da3d50ae/promql/parser/generated_parser.y#L679 - public static TokenListParser LabelValueMatcher = + public static TokenListParser> LabelValueMatcher = from id in Token.EqualTo(PromToken.IDENTIFIER) - .Or(Token.EqualTo(PromToken.AGGREGATE_OP).Where(x => Operators.Aggregates.Contains(x.ToStringValue()))) + .Or(Token.EqualTo(PromToken.AGGREGATE_OP).Where(x => Operators.Aggregates.ContainsKey(x.ToStringValue()))) // Inside of grouping options label names can be recognized as keywords by the lexer. This is a list of keywords that could also be a label name. // See https://github.com/prometheus/prometheus/blob/7471208b5c8ff6b65b644adedf7eb964da3d50ae/promql/parser/generated_parser.y#L678 for more info. .Or(Token.Matching(t => KeywordAndAlphanumericOperatorTokens.Contains(t), "keyword_or_operator")) .Or(Token.EqualTo(PromToken.OFFSET)) - select id.ToStringValue(); + select new ParsedValue(id.ToStringValue(), id.Span); public static TokenListParser LabelMatcher = from id in LabelValueMatcher from op in MatchOp from str in StringLiteral - select new LabelMatcher(id, op, (StringLiteral)str); + select new LabelMatcher(id.Value, op, (StringLiteral)str, id.Span.UntilEnd(str.Span)); public static TokenListParser MatchOp = Token.EqualTo(PromToken.EQL).Select(_ => Operators.LabelMatch.Equal) @@ -109,7 +111,7 @@ from str in StringLiteral public static TokenListParser Number = from s in ( Token.EqualTo(PromToken.ADD).Or(Token.EqualTo(PromToken.SUB)) - ).OptionalOrDefault(new Token(PromToken.ADD, TextSpan.Empty)) + ).OptionalOrDefault(new Token(PromToken.ADD, TextSpan.None)) from n in Token.EqualTo(PromToken.NUMBER) select new NumberLiteral( (n.ToStringValue(), s.Kind) switch @@ -117,7 +119,8 @@ from n in Token.EqualTo(PromToken.NUMBER) (var v, PromToken.ADD) when v.Equals("Inf", StringComparison.OrdinalIgnoreCase) => double.PositiveInfinity, (var v, PromToken.SUB) when v.Equals("Inf", StringComparison.OrdinalIgnoreCase) => double.NegativeInfinity, (var v, var op) => double.Parse(v) * (op == PromToken.SUB ? -1.0 : 1.0) - } + }, + s.Span.Length > 0 ? s.Span.UntilEnd(n.Span) : n.Span ); /// @@ -153,7 +156,7 @@ static TimeSpan ParseComponent(Match m, int index, Func parser) ts += ParseComponent(match, 12, i => TimeSpan.FromSeconds(i)); ts += ParseComponent(match, 14, i => TimeSpan.FromMilliseconds(i)); - return new Duration(ts); + return new Duration(ts, n.Span); }); // TODO support unicode, octal and hex escapes @@ -197,43 +200,71 @@ from close in Character.EqualTo('`') { var c = t.Span.ConsumeChar(); if (c.Value == '\'') - return new StringLiteral('\'', SingleQuoteStringLiteral.Parse(t.Span.ToStringValue())); + return new StringLiteral('\'', SingleQuoteStringLiteral.Parse(t.Span.ToStringValue()), t.Span); if (c.Value == '"') - return new StringLiteral('"', DoubleQuoteStringLiteral.Parse(t.Span.ToStringValue())); + return new StringLiteral('"', DoubleQuoteStringLiteral.Parse(t.Span.ToStringValue()), t.Span); if (c.Value == '`') - return new StringLiteral('`', RawString.Parse(t.Span.ToStringValue())); + return new StringLiteral('`', RawString.Parse(t.Span.ToStringValue()), t.Span); throw new ParseException($"Unexpected string quote", t.Span.Position); }); + private static readonly HashSet ValidOffsetExpressions = new HashSet + { + typeof(MatrixSelector), + typeof(VectorSelector), + typeof(SubqueryExpr), + }; public static Func> OffsetExpr = (Expr expr) => + ( from offset in Token.EqualTo(PromToken.OFFSET) from neg in Token.EqualTo(PromToken.SUB).Optional() from duration in Duration - select new OffsetExpr(expr, new Duration(new TimeSpan(duration.Value.Ticks * (neg.HasValue ? -1 : 1)))); + // Where needs to be called once the parser has definitely been advanced beyond the initial token (offset) + // it parses in order for Or() to consider this a partial failure + .Where(_ => + ValidOffsetExpressions.Contains(expr.GetType()), + "offset modifier must be preceded by an instant vector selector or range vector selector or a subquery" + ) + select new OffsetExpr( + expr, + new Duration(new TimeSpan(duration.Value.Ticks * (neg.HasValue ? -1 : 1))), + expr.Span!.Value.UntilEnd(duration.Span) + ) + ); public static TokenListParser ParenExpression = - from e in Parse.Ref(() => Expr).Between(Token.EqualTo(PromToken.LEFT_PAREN), Token.EqualTo(PromToken.RIGHT_PAREN)) - select new ParenExpression(e); - - public static TokenListParser FunctionArgs = Parse.Ref(() => Expr).ManyDelimitedBy(Token.EqualTo(PromToken.COMMA)) - .Between(Token.EqualTo(PromToken.LEFT_PAREN).Try(), Token.EqualTo(PromToken.RIGHT_PAREN)); + from lp in Token.EqualTo(PromToken.LEFT_PAREN) + from e in Parse.Ref(() => Expr) + from rp in Token.EqualTo(PromToken.RIGHT_PAREN) + select new ParenExpression(e, lp.Span.UntilEnd(rp.Span)); + + public static TokenListParser> FunctionArgs = + from lp in Token.EqualTo(PromToken.LEFT_PAREN).Try() + from args in Parse.Ref(() => Expr).ManyDelimitedBy(Token.EqualTo(PromToken.COMMA)) + from rp in Token.EqualTo(PromToken.RIGHT_PAREN) + select args.ToParsedValue(lp.Span, rp.Span); public static TokenListParser FunctionCall = - from id in Token.EqualTo(PromToken.IDENTIFIER).Where(x => Functions.Names.Contains(x.ToStringValue())).Try() + from id in Token.EqualTo(PromToken.IDENTIFIER).Where(x => Functions.Map.ContainsKey(x.ToStringValue())).Try() + let function = Functions.Map[id.ToStringValue()] from args in FunctionArgs - select new FunctionCall(id.ToStringValue(), args.ToImmutableArray()); + .Where(a => function.IsVariadic || (!function.IsVariadic && function.ArgTypes.Length == a.Value.Length), $"Incorrect number of argument(s) in call to {function.Name}, expected {function.ArgTypes.Length} argument(s)") + .Where(a => !function.IsVariadic || (function.IsVariadic && a.Value.Length >= function.MinArgCount), $"Incorrect number of argument(s) in call to {function.Name}, expected at least {function.MinArgCount} argument(s)") + // TODO validate "at most" arguments- https://github.com/prometheus/prometheus/blob/7471208b5c8ff6b65b644adedf7eb964da3d50ae/promql/parser/parse.go#L552 + select new FunctionCall(function, args.Value.ToImmutableArray(), id.Span.UntilEnd(args.Span)); - public static TokenListParser> GroupingLabels = + public static TokenListParser>> GroupingLabels = + from lParen in Token.EqualTo(PromToken.LEFT_PAREN) from labels in (LabelValueMatcher.ManyDelimitedBy(Token.EqualTo(PromToken.COMMA))) - .Between(Token.EqualTo(PromToken.LEFT_PAREN), Token.EqualTo(PromToken.RIGHT_PAREN)) - select labels.Select(x => x).ToImmutableArray(); + from rParen in Token.EqualTo(PromToken.RIGHT_PAREN) + select labels.Select(x => x.Value).ToImmutableArray().ToParsedValue(lParen.Span, rParen.Span); - public static TokenListParser BoolModifier = + public static TokenListParser> BoolModifier = from b in Token.EqualTo(PromToken.BOOL).Optional() - select b.HasValue; + select b.HasValue.ToParsedValue(b?.Span ?? TextSpan.None); public static TokenListParser OnOrIgnoring = from b in BoolModifier @@ -241,10 +272,11 @@ from onOrIgnoring in Token.EqualTo(PromToken.ON).Or(Token.EqualTo(PromToken.IGNO from onOrIgnoringLabels in GroupingLabels select new VectorMatching( Operators.VectorMatchCardinality.OneToOne, - onOrIgnoringLabels, + onOrIgnoringLabels.Value, onOrIgnoring.HasValue && onOrIgnoring.Kind == PromToken.ON, ImmutableArray.Empty, - b + b.Value, + b.HasSpan ? b.Span.UntilEnd(onOrIgnoringLabels.Span) : onOrIgnoring.Span.UntilEnd(onOrIgnoringLabels.Span) ); public static Func> SubqueryExpr = (Expr expr) => @@ -253,13 +285,13 @@ from range in Duration from colon in Token.EqualTo(PromToken.COLON) from step in Duration.AsNullable().OptionalOrDefault() from rb in Token.EqualTo(PromToken.RIGHT_BRACKET) - select new SubqueryExpr(expr, range, step); + select new SubqueryExpr(expr, range, step, expr.Span!.Value.UntilEnd(rb.Span)); public static TokenListParser VectorMatching = from vectMatching in ( from vm in OnOrIgnoring from grp in Token.EqualTo(PromToken.GROUP_LEFT).Or(Token.EqualTo(PromToken.GROUP_RIGHT)) - from grpLabels in GroupingLabels.OptionalOrDefault(ImmutableArray.Empty) + from grpLabels in GroupingLabels.OptionalOrDefault(ImmutableArray.Empty.ToEmptyParsedValue()) select vm with { MatchCardinality = grp switch @@ -269,14 +301,15 @@ select vm with {Kind: PromToken.GROUP_RIGHT} => Operators.VectorMatchCardinality.OneToMany, _ => Operators.VectorMatchCardinality.OneToOne }, - Include = grpLabels + Include = grpLabels.Value, + Span = vm.Span!.Value.UntilEnd(grpLabels.HasSpan ? grpLabels.Span : grp.Span) } ).Try().Or( from vm in OnOrIgnoring select vm ).Try().Or( from b in BoolModifier - select new VectorMatching(b) + select new VectorMatching(b.Value) { Span = b.Span } ) select vectMatching; @@ -301,32 +334,107 @@ from b in BoolModifier }; public static TokenListParser BinaryExpr = - from lhs in Parse.Ref(() => ExprNotBinary) - from op in Token.Matching(x => BinaryOperatorMap.ContainsKey(x), "binary_op") - from vm in VectorMatching.AsNullable().OptionalOrDefault() - from rhs in Parse.Ref(() => Expr) - select new BinaryExpr(lhs, rhs, BinaryOperatorMap[op.Kind], vm); + // Sprache doesn't support lef recursive grammars so we have to parse out binary expressions as lists of non-binary expressions + from head in Parse.Ref(() => ExprNotBinary) + from tail in ( + from opToken in Token.Matching(x => BinaryOperatorMap.ContainsKey(x), "binary_op") + let op = BinaryOperatorMap[opToken.Kind] + from vm in VectorMatching.AsNullable().OptionalOrDefault() + .Where(x => x is not { ReturnBool: true } || (x.ReturnBool && Operators.BinaryComparisonOperators.Contains(op)), "bool modifier can only be used on comparison operators") + from expr in Parse.Ref(() => ExprNotBinary) + select (op, vm, expr) + ).AtLeastOnce() + select CreateBinaryExpression(head, tail); + + /// + /// Creates a binary expression from a collection of two or more operands and one or more operators. + /// + /// + /// This function need to ensure operator precedence is maintained, e.g. 1 + 2 * 3 or 4 should parsed as (1 + (2 * 3)) or 4. + /// + /// The first operand + /// The trailing operators, vector matching + operands + private static BinaryExpr CreateBinaryExpression(Expr head, (Operators.Binary op, VectorMatching? vm, Expr expr)[] tail) + { + // Just two operands, no need to do any precedence checking + if (tail.Length == 1) + return new BinaryExpr(head, tail[0].expr, tail[0].op, tail[0].vm, head.Span!.Value.UntilEnd(tail[0].expr.Span)); + + // Three + operands and we need to group subexpressions by precedence. First things first: create linked lists of all our operands and operators + var operands = new LinkedList(new[] { head }.Concat(tail.Select(x => x.expr))); + var operators = new LinkedList<(Operators.Binary op, VectorMatching? vm)>(tail.Select(x => (x.op, x.vm))); + + // Iterate through each level of operator precedence, moving from highest -> lowest + foreach (var precedenceLevel in Operators.BinaryPrecedence) + { + var lhs = operands.First; + var op = operators.First; + + // While we have operators left to consume, iterate through each operand + operator + while (op != null) + { + var rhs = lhs!.Next!; + + // This operator has the same precedence of the current precedence level- create a new binary subexpression with the current operands + operators + if (precedenceLevel.Contains(op.Value.op)) + { + var b = new BinaryExpr(lhs.Value, rhs.Value, op.Value.op, op.Value.vm, lhs.Value.Span!.Value.UntilEnd(rhs.Value.Span)); // TODO span matching + var bNode = operands.AddBefore(rhs, b); + + // Remove the previous operands (will replace with our new binary expression) + operands.Remove(lhs); + operands.Remove(rhs); + + lhs = bNode; + var nextOp = op.Next; + + // Remove the operator + operators.Remove(op); + op = nextOp; + } + else + { + // Move on to the next operand + operator + lhs = rhs; + op = op.Next; + } + } + } + + return (BinaryExpr)operands.Single(); + } - public static TokenListParser labels)> AggregateModifier = + public static TokenListParser labels)>> AggregateModifier = from kind in Token.EqualTo(PromToken.BY).Try() .Or(Token.EqualTo(PromToken.WITHOUT).Try()) from labels in GroupingLabels - select (kind.Kind == PromToken.WITHOUT, labels); + select (kind.Kind == PromToken.WITHOUT, labels.Value).ToParsedValue(kind.Span, labels.Span); public static TokenListParser AggregateExpr = - from op in Token.EqualTo(PromToken.AGGREGATE_OP).Where(x => Operators.Aggregates.Contains(x.Span.ToStringValue())).Try() + from op in Token.EqualTo(PromToken.AGGREGATE_OP) + .Where(x => Operators.Aggregates.ContainsKey(x.ToStringValue())).Try() + let aggOps = Operators.Aggregates[op.ToStringValue()] from argsAndMod in ( from args in FunctionArgs - from mod in AggregateModifier.OptionalOrDefault((without: false, labels: ImmutableArray.Empty)) - select (mod, args) + from mod in AggregateModifier.OptionalOrDefault( + (without: false, labels: ImmutableArray.Empty).ToEmptyParsedValue() + ) + select (mod, args: args.Value).ToParsedValue(args.Span, mod.HasSpan ? mod.Span : args.Span) ).Or( from mod in AggregateModifier from args in FunctionArgs - select (mod, args) + select (mod, args: args.Value).ToParsedValue(mod.Span, args.Span) ) - .Where(x => x.args.Length >= 1, "At least one argument is required for aggregate expressions") - .Where(x => x.args.Length <= 2, "A maximum of two arguments is supported for aggregate expressions") - select new AggregateExpr(op.ToStringValue(), argsAndMod.args.Length > 1 ? argsAndMod.args[1] : argsAndMod.args[0], argsAndMod.args.Length > 1 ? argsAndMod.args[0] : null, argsAndMod.mod.labels, argsAndMod.mod.without ); + .Where(x => aggOps.ParameterType == null || (aggOps.ParameterType != null && x.Value.args.Length == 2), "wrong number of arguments for aggregate expression provided, expected 2, got 1") + .Where(x => aggOps.ParameterType != null || (aggOps.ParameterType == null && x.Value.args.Length == 1), "wrong number of arguments for aggregate expression provided, expected 1, got 2") + select new AggregateExpr( + aggOps, + argsAndMod.Value.args.Length > 1 ? argsAndMod.Value.args[1] : argsAndMod.Value.args[0], + argsAndMod.Value.args.Length > 1 ? argsAndMod.Value.args[0] : null, + argsAndMod.Value.mod.Value.labels, + argsAndMod.Value.mod.Value.without, + Span: op.Span.UntilEnd(argsAndMod.Span) + ); public static TokenListParser ExprNotBinary = from head in OneOf( @@ -341,23 +449,26 @@ from head in OneOf( Parse.Ref(() => StringLiteral).Cast() ) #pragma warning disable CS8602 - from offsetOrSubquery in Parse.Ref(() => OffsetOrSubquery(head)).AsNullable().OptionalOrDefault() + from offsetOrSubquery in Parse.Ref(() => OffsetOrSubquery(head)) #pragma warning restore CS8602 select offsetOrSubquery == null ? head : offsetOrSubquery; - public static Func> OffsetOrSubquery = (Expr expr) => - from offsetOfSubquery in ( - from offset in OffsetExpr(expr) - select (Expr)offset - ).Or( - from subquery in SubqueryExpr(expr) - select (Expr)subquery - ) - select offsetOfSubquery; + public static Func> OffsetOrSubquery = (Expr expr) => + ( + from offset in OffsetExpr(expr) + select (Expr) offset + ).Or( + from subquery in SubqueryExpr(expr) + select (Expr) subquery + ) + .AsNullable() + .Or( + Parse.Return(null) + ); public static TokenListParser Expr { get; } = from head in Parse.Ref(() => BinaryExpr).Cast().Try().Or(ExprNotBinary) - from offsetOrSubquery in OffsetOrSubquery(head).AsNullable().OptionalOrDefault() + from offsetOrSubquery in OffsetOrSubquery(head) select offsetOrSubquery == null ? head : offsetOrSubquery; /// @@ -386,4 +497,46 @@ private static TokenListParser OneOf(params TokenListParser + { + public ParsedValue(T value, TextSpan span) + { + Value = value; + Span = span; + } + + public T Value { get; } + public bool HasSpan => Span != TextSpan.None; + public TextSpan Span { get; } + } + + public static class Extensions + { + public static ParsedValue ToParsedValue(this T result, TextSpan start, TextSpan end) + { + return new ParsedValue(result, start.UntilEnd(end)); + } + + public static ParsedValue ToEmptyParsedValue(this T result) + { + return new ParsedValue(result, TextSpan.None); + } + + public static TextSpan UntilEnd(this TextSpan @base, TextSpan? next) + { + if (next == null) + return @base; + + int absolute1 = next.Value.Position.Absolute + next.Value.Length; + int absolute2 = @base.Position.Absolute; + return @base.First(absolute1 - absolute2); + } + + public static ParsedValue ToParsedValue(this T result, TextSpan span) + { + return new ParsedValue(result, span); + } + + } } diff --git a/src/PromQL.Parser/Printer.cs b/src/PromQL.Parser/Printer.cs index 83fa20a..1e3d4ca 100644 --- a/src/PromQL.Parser/Printer.cs +++ b/src/PromQL.Parser/Printer.cs @@ -4,6 +4,8 @@ namespace PromQL.Parser { + // TODO fix bug around "and" + // TODO pretty print support public class Printer : IVisitor { private StringBuilder _sb = new (); @@ -132,7 +134,7 @@ public virtual void Visit(ParenExpression paren) public virtual void Visit(FunctionCall fnCall) { - _sb.Append($"{fnCall.Identifier}("); + _sb.Append($"{fnCall.Function.Name}("); bool isFirst = true; foreach (var arg in fnCall.Args) @@ -199,7 +201,7 @@ public virtual void Visit(BinaryExpr expr) public virtual void Visit(AggregateExpr expr) { - _sb.Append($"{expr.OperatorName}"); + _sb.Append($"{expr.Operator.Name}"); if (expr.GroupingLabels.Length > 0) { diff --git a/src/PromQL.Parser/Tokenizer.cs b/src/PromQL.Parser/Tokenizer.cs index 6d1cdc5..d3c1af6 100644 --- a/src/PromQL.Parser/Tokenizer.cs +++ b/src/PromQL.Parser/Tokenizer.cs @@ -87,7 +87,7 @@ from close in Character.EqualTo(quoteChar) .Select(x => { var idOrKeyword = x.ToStringValue(); - if (Operators.Aggregates.Contains(idOrKeyword)) + if (Operators.Aggregates.ContainsKey(idOrKeyword)) return PromToken.AGGREGATE_OP; if (KeywordsToTokens.TryGetValue(idOrKeyword, out var keyToken)) return keyToken; diff --git a/src/PromQL.Parser/TypeChecker.cs b/src/PromQL.Parser/TypeChecker.cs new file mode 100644 index 0000000..22b30d7 --- /dev/null +++ b/src/PromQL.Parser/TypeChecker.cs @@ -0,0 +1,117 @@ +using System; +using System.Linq; +using ExhaustiveMatching; +using PromQL.Parser.Ast; +using Superpower.Model; + +namespace PromQL.Parser +{ + public static class TypeChecker + { + public static ValueType CheckType(this Expr expr) + { + switch (expr) + { + case AggregateExpr aggExpr: + // TODO Currently don't check for parameter counts here + // The parser does this currently but we might want to extend the logic for user generated ASTs. + if (aggExpr.Operator.ParameterType != null) + ExpectTypes(aggExpr.Param!, aggExpr.Operator.ParameterType.Value); + + ExpectTypes(aggExpr.Expr, ValueType.Vector); + + return aggExpr.Type; + + case BinaryExpr binExpr: + var lhsType = ExpectTypes(binExpr.LeftHandSide, ValueType.Scalar, ValueType.Vector); + var rhsType = ExpectTypes(binExpr.RightHandSide, ValueType.Scalar, ValueType.Vector); + + if (Operators.BinaryComparisonOperators.Contains(binExpr.Operator) && !(binExpr.VectorMatching?.ReturnBool ?? false) + && lhsType == ValueType.Scalar && rhsType == ValueType.Scalar) + { + throw new InvalidTypeException("comparisons between scalars must use bool modifier", binExpr.Span); + } + + + // TODO https://github.com/prometheus/prometheus/blob/7471208b5c8ff6b65b644adedf7eb964da3d50ae/promql/parser/parse.go#L526-L534 + + if ((lhsType == ValueType.Scalar || rhsType == ValueType.Scalar) && Operators.BinarySetOperators.Contains(binExpr.Operator)) + throw new InvalidTypeException($"set operator {binExpr.Operator} not allowed in binary scalar expression", binExpr.Span); + + return binExpr.Type; + + case FunctionCall fnCall: + var expectedArgTypes = fnCall.Function.ArgTypes; + + // Varadic functions can repeat the last parameter type indefinitely + if (fnCall.Function.IsVariadic) + expectedArgTypes = expectedArgTypes.AddRange( + Enumerable.Repeat( + fnCall.Function.ArgTypes.Last(), + fnCall.Args.Length - fnCall.Function.MinArgCount + ) + ); + + foreach (var (arg, expectedType) in fnCall.Args.Zip(expectedArgTypes)) + ExpectTypes(arg, expectedType); + + return fnCall.Type; + + case SubqueryExpr subqueryExpr: + ExpectTypes(subqueryExpr.Expr, ValueType.Vector); + return subqueryExpr.Type; + + case UnaryExpr unaryExpr: + ExpectTypes(unaryExpr.Expr, ValueType.Scalar, ValueType.Vector); + return unaryExpr.Type; + + case OffsetExpr offsetExpr: + return offsetExpr.Expr.CheckType(); + + case ParenExpression parenExpr: + return parenExpr.Expr.CheckType(); + + case NumberLiteral _: + case StringLiteral _: + case MatrixSelector _: + case VectorSelector _: + return expr.Type; + + default: + throw ExhaustiveMatch.Failed(expr); + } + } + + private static ValueType ExpectTypes(Expr expr, params ValueType[] expected) + { + var type = expr.CheckType(); + if (!expected.Contains(type)) + throw new InvalidTypeException(expected, expr.Type, expr.Span); + + return type; + } + + public class InvalidTypeException : Exception + { + public InvalidTypeException(ValueType[] expected, ValueType provided, TextSpan? span) + : this($"Unexpected type '{AsHumanReadable(provided)}' was provided, expected {(expected.Length == 1 ? AsHumanReadable(expected[0]): $"one of {string.Join(", ", expected.Select(e => $"'{AsHumanReadable(e)}'"))}")}", span) + { + } + + public InvalidTypeException(string message, TextSpan? span) + : base($"{message}{(span.HasValue ? $": {span.Value.Position.ToString()}" : "")}") + { + } + + private static string AsHumanReadable(ValueType vt) => vt switch + { + ValueType.Scalar => "scalar", + ValueType.Matrix => "range vector", + ValueType.Vector => "instant vector", + ValueType.String => "string", + ValueType.None => "none", + _ => throw ExhaustiveMatch.Failed(vt) + }; + } + } +} \ No newline at end of file diff --git a/src/PromQL.Parser/ValueType.cs b/src/PromQL.Parser/ValueType.cs new file mode 100644 index 0000000..e5474d8 --- /dev/null +++ b/src/PromQL.Parser/ValueType.cs @@ -0,0 +1,14 @@ +namespace PromQL.Parser +{ + /// + /// Taken from https://github.com/prometheus/prometheus/blob/277bf93952b56227cb750a8129197efa489eddde/promql/parser/value.go#L26-L32. + /// + public enum ValueType + { + None, + Scalar, + Vector, + Matrix, + String, + } +} \ No newline at end of file diff --git a/tests/PromQL.Parser.Tests/ParserTests.cs b/tests/PromQL.Parser.Tests/ParserTests.cs index d717229..7a1c422 100644 --- a/tests/PromQL.Parser.Tests/ParserTests.cs +++ b/tests/PromQL.Parser.Tests/ParserTests.cs @@ -1,12 +1,10 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Linq; using FluentAssertions; using NUnit.Framework; using PromQL.Parser.Ast; using Superpower; -using Superpower.Display; using Superpower.Model; namespace PromQL.Parser.Tests @@ -25,11 +23,11 @@ public void SetUp() [Test] public void StringLiteralDoubleQuote() => Parse(Parser.StringLiteral, "\"A string\"") - .Should().Be(new StringLiteral('"', "A string")); + .Should().Be(new StringLiteral('"', "A string", new TextSpan("\"A string\""))); [Test] public void StringLiteralDoubleQuoteEscaped() => Parse(Parser.StringLiteral, @"""\a\b\f\n\r\t\v\\\""""") - .Should().Be(new StringLiteral('"', "\a\b\f\n\r\t\v\\\"")); + .Should().Be(new StringLiteral('"', "\a\b\f\n\r\t\v\\\"", new TextSpan(@"""\a\b\f\n\r\t\v\\\"""""))); [Test] public void StringLiteralDoubleQuoteNewline() => @@ -37,11 +35,11 @@ public void StringLiteralDoubleQuoteNewline() => [Test] public void StringLiteralSingleQuote() => Parse(Parser.StringLiteral, "'A string'") - .Should().Be(new StringLiteral('\'', "A string")); + .Should().Be(new StringLiteral('\'', "A string", new TextSpan("'A string'"))); [Test] public void StringLiteralSingleQuoteEscaped() => Parse(Parser.StringLiteral, @"'\a\b\f\n\r\t\v\\\''") - .Should().Be(new StringLiteral('\'', "\a\b\f\n\r\t\v\\\'")); + .Should().Be(new StringLiteral('\'', "\a\b\f\n\r\t\v\\\'", new TextSpan(@"'\a\b\f\n\r\t\v\\\''"))); [Test] public void StringLiteralSingleQuoteNewline() => @@ -49,15 +47,15 @@ public void StringLiteralSingleQuoteNewline() => [Test] public void StringLiteralRaw() => Parse(Parser.StringLiteral, "`A string`") - .Should().Be(new StringLiteral('`', "A string")); + .Should().Be(new StringLiteral('`', "A string", new TextSpan("`A string`"))); [Test] public void StringLiteralRaw_Multiline() => Parse(Parser.StringLiteral, "`A\n string`") - .Should().Be(new StringLiteral('`', "A\n string")); + .Should().Be(new StringLiteral('`', "A\n string", new TextSpan("`A\n string`"))); [Test] public void StringLiteralRaw_NoEscapes() => Parse(Parser.StringLiteral, @"`\a\b\f\n\r\t\v`") - .Should().Be(new StringLiteral('`', @"\a\b\f\n\r\t\v")); + .Should().Be(new StringLiteral('`', @"\a\b\f\n\r\t\v", new TextSpan(@"`\a\b\f\n\r\t\v`"))); [Test] [TestCase("2y52w365d25h10m30s100ms", "1460.01:10:30.100")] @@ -67,7 +65,7 @@ public void StringLiteralRaw_NoEscapes() => Parse(Parser.StringLiteral, @"`\a\b\ [TestCase("180s", "00:03:00")] [TestCase("500ms", "00:00:00.500")] public void Duration(string input, string expected) => Parse(Parser.Duration, input) - .Should().Be(new Duration(TimeSpan.Parse(expected))); + .Should().Be(new Duration(TimeSpan.Parse(expected), new TextSpan(input))); [Test] @@ -80,37 +78,38 @@ public void Duration(string input, string expected) => Parse(Parser.Duration, in [TestCase("1e5", 100000)] [TestCase("1.55E+5", 155000)] public void Number(string input, double expected) => Parse(Parser.Number, input) - .Should().Be(new NumberLiteral(expected)); + .Should().Be(new NumberLiteral(expected, new TextSpan(input))); [Test] [TestCase("nan")] [TestCase("NaN")] public void Number_NaN(string input) => Parse(Parser.Number, input) - .Should().Be(new NumberLiteral(double.NaN)); + .Should().Be(new NumberLiteral(double.NaN, new TextSpan(input))); [Test] [TestCase("Inf")] [TestCase("+inf")] public void Number_InfPos(string input) => Parse(Parser.Number, input) - .Should().Be(new NumberLiteral(double.PositiveInfinity)); + .Should().Be(new NumberLiteral(double.PositiveInfinity, new TextSpan(input))); [Test] [TestCase("-Inf")] [TestCase("-inf")] public void Number_InfNeg(string input) => Parse(Parser.Number, input) - .Should().Be(new NumberLiteral(double.NegativeInfinity)); + .Should().Be(new NumberLiteral(double.NegativeInfinity, new TextSpan(input))); [Test] [TestCaseSource(nameof(FunctionsAggregatesAndOperators))] public void LabelValueMatcher_FunctionsOperatorsAndKeywords(string identifier) => - Parse(Parser.LabelValueMatcher, identifier).Should().Be(identifier); + Parse(Parser.LabelValueMatcher, identifier).Value.Should().Be(identifier); [Test] public void LabelMatchers_Empty() { - Parse(Parser.LabelMatchers, "{}") - .Should().Be(new LabelMatchers(ImmutableArray.Empty)); + const string input = "{}"; + Parse(Parser.LabelMatchers, input) + .Should().Be(new LabelMatchers(ImmutableArray.Empty, new TextSpan(input))); } [Test] @@ -122,86 +121,143 @@ public void LabelMatchers_EmptyNoComma() [Test] public void LabelMatchers_One() { - Parse(Parser.LabelMatchers, "{blah=\"my_label\"}") - .Should().BeEquivalentTo(new LabelMatchers(new [] - { - new LabelMatcher("blah", Operators.LabelMatch.Equal, new StringLiteral('"', "my_label")) - }.ToImmutableArray())); + const string input = "{blah=\"my_label\"}"; + Parse(Parser.LabelMatchers, input) + .Should().BeEquivalentTo( + new LabelMatchers(new [] + { + new LabelMatcher( + "blah", + Operators.LabelMatch.Equal, + new StringLiteral('"', "my_label", new TextSpan(input, new Position(6, 0, 0), 10)), + new TextSpan(input, new Position(1, 0, 0), 15) + ) + }.ToImmutableArray(), + Span: new TextSpan(input) + ) + ); } [Test] public void LabelMatchers_OneTrailingComma() { - Parse(Parser.LabelMatchers, "{blah=\"my_label\" , }") - .Should().BeEquivalentTo(new LabelMatchers(new[] - { - new LabelMatcher("blah", Operators.LabelMatch.Equal, new StringLiteral('"', "my_label")) - }.ToImmutableArray())); + const string input = "{blah=\"my_label\" , }"; + Parse(Parser.LabelMatchers, input) + .Should().BeEquivalentTo( + new LabelMatchers( + new[] + { + new LabelMatcher( + "blah", + Operators.LabelMatch.Equal, + new StringLiteral('"', "my_label", new TextSpan(input, new Position(6, 0, 0), 10)), + new TextSpan(input, new Position(1, 0, 0), 15) + ), + }.ToImmutableArray(), + new TextSpan(input) + ) + ); } [Test] public void LabelMatchers_Many() { Parse(Parser.LabelMatchers, "{ blah=\"my_label\", blah_123 != 'my_label', b123=~'label', b_!~'label' }") - .Should().BeEquivalentTo(new LabelMatchers(new [] - { - new LabelMatcher("blah", Operators.LabelMatch.Equal, new StringLiteral('"', "my_label")), - new LabelMatcher("blah_123", Operators.LabelMatch.NotEqual, new StringLiteral('\'', "my_label")), - new LabelMatcher("b123", Operators.LabelMatch.Regexp, new StringLiteral('\'', "label")), - new LabelMatcher("b_", Operators.LabelMatch.NotRegexp, new StringLiteral('\'', "label")) - }.ToImmutableArray())); + .Should().BeEquivalentTo( + new LabelMatchers( + new [] + { + new LabelMatcher("blah", Operators.LabelMatch.Equal, new StringLiteral('"', "my_label")), + new LabelMatcher("blah_123", Operators.LabelMatch.NotEqual, new StringLiteral('\'', "my_label")), + new LabelMatcher("b123", Operators.LabelMatch.Regexp, new StringLiteral('\'', "label")), + new LabelMatcher("b_", Operators.LabelMatch.NotRegexp, new StringLiteral('\'', "label")) + }.ToImmutableArray() + ), + // Don't assert over parsed Span positions, will be tedious to specify all positions + cfg => cfg.Excluding(x => x.Name == "Span") + ); } [Test] public void VectorSelector_MetricIdentifier() { - Parse(Parser.VectorSelector, ":this_is_a_metric") - .Should().BeEquivalentTo(new VectorSelector(new MetricIdentifier(":this_is_a_metric"))); + const string input = ":this_is_a_metric"; + Parse(Parser.VectorSelector, input) + .Should().BeEquivalentTo( + new VectorSelector(new MetricIdentifier(input, new TextSpan(input)), new TextSpan(input)) + ); } [Test] public void VectorSelector_MetricIdentifier_And_LabelMatchers() { - Parse(Parser.VectorSelector, ":this_is_a_metric { }") - .Should().BeEquivalentTo(new VectorSelector(new MetricIdentifier(":this_is_a_metric"), new LabelMatchers(ImmutableArray.Empty))); + const string input = ":this_is_a_metric { }"; + Parse(Parser.VectorSelector, input) + .Should().BeEquivalentTo( + new VectorSelector( + new MetricIdentifier(":this_is_a_metric", new TextSpan(input, new Position(0, 0, 0), 17)), + new LabelMatchers(ImmutableArray.Empty, new TextSpan(input, new Position(18, 0, 0), 3)), + new TextSpan(input) + ) + ); } [Test] public void VectorSelector_LabelMatchers() { - Parse(Parser.VectorSelector, "{ }") - .Should().BeEquivalentTo(new VectorSelector(new LabelMatchers(ImmutableArray.Empty))); + const string input = "{ }"; + Parse(Parser.VectorSelector, input) + .Should().BeEquivalentTo( + new VectorSelector( + new LabelMatchers(ImmutableArray.Empty, new TextSpan(input)), + new TextSpan(input) + ) + ); } [Test] - public void MatrixSelector() => Parse(Parser.MatrixSelector, " metric { } [ 1m1s ] ") - .Should().Be(new MatrixSelector( - new VectorSelector(new MetricIdentifier("metric"), new LabelMatchers(ImmutableArray.Empty)), - new Duration(new TimeSpan(0, 1, 1)) - )); + public void MatrixSelector() + { + var input = "metric { } [ 1m1s ]"; + Parse(Parser.MatrixSelector, input) + .Should().Be(new MatrixSelector( + new VectorSelector( + new MetricIdentifier("metric", new TextSpan(input, Position.Zero, 6)), + new LabelMatchers(ImmutableArray.Empty, new TextSpan(input, new Position(7, 0, 0), 3)), + new TextSpan(input, Position.Zero, 10) + ), + new Duration(new TimeSpan(0, 1, 1), new TextSpan(input, new Position(13, 0, 0), 4)), + new TextSpan(input) + )); + } [Test] public void Offset_Vector() { - var expr = Parse(Parser.Expr, " metric { } offset 1m"); + const string input = "metric { } offset 1m"; + var expr = Parse(Parser.Expr, input); var offsetExpr = expr.Should().BeOfType().Subject; offsetExpr.Expr.Should().BeOfType(); offsetExpr.Duration.Value.Should().Be(TimeSpan.FromMinutes(1)); + offsetExpr.Span.Should().Be(new TextSpan(input)); } [Test] public void Offset_MatrixSelector() { - var expr = Parse(Parser.Expr, " metric { } [ 1m1s ] offset -7m"); + const string input = "metric { } [ 1m1s ] offset -7m"; + var expr = Parse(Parser.Expr, input); var offsetExpr = expr.Should().BeOfType().Subject; offsetExpr.Expr.Should().BeOfType(); offsetExpr.Duration.Value.Should().Be(TimeSpan.FromMinutes(-7)); + offsetExpr.Span.Should().Be(new TextSpan(input)); } [Test] public void Offset_Subquery() { - var expr = Parse(Parser.Expr, " metric[ 1h:1m ] offset 1w"); + var input = "metric[ 1h:1m ] offset 1w"; + var expr = Parse(Parser.Expr, input); expr.Should().BeEquivalentTo( new OffsetExpr( new SubqueryExpr( @@ -210,14 +266,29 @@ public void Offset_Subquery() new Duration(TimeSpan.FromMinutes(1)) ), new Duration(TimeSpan.FromDays(7)) - ) + ), + // Don't assert over parsed Span positions, will be tedious to specify all positions + cfg => cfg.Excluding(x => x.Name == "Span") ); + expr.Span.Should().Be(new TextSpan(input)); } + [Test] + [TestCase("time() offset 1m")] + [TestCase("avg(blah) offset 1m")] + [TestCase("'asdas' offset 1m")] + [TestCase("1 offset 1m")] + public void Offset_Invalid(string badQuery) + { + Assert.Throws(() => Parse(Parser.Expr, badQuery)) + .Message.Should().Contain("offset modifier must be preceded by an instant vector selector or range vector selector or a subquery"); + } + [Test] public void Subquery_Offset() { - var expr = Parse(Parser.Expr, " metric offset 1w [ 1h:1m ]"); + const string input = "metric offset 1w [ 1h:1m ]"; + var expr = Parse(Parser.Expr, input); expr.Should().BeEquivalentTo( new SubqueryExpr( new OffsetExpr( @@ -226,8 +297,11 @@ public void Subquery_Offset() ), new Duration(TimeSpan.FromHours(1)), new Duration(TimeSpan.FromMinutes(1)) - ) + ), + // Don't assert over parsed Span positions, will be tedious to specify all positions + cfg => cfg.Excluding(x => x.Name == "Span") ); + expr.Span.Should().Be(new TextSpan(input)); } [Test] @@ -246,59 +320,133 @@ public void ParenExpr_MissingLeft() public void ParenExpr_MissingRight() { Assert.Throws(() => Parse(Parser.Expr, " (( 1 ) ")); - } - - [Test] - public void ParenExpr_Simple() => Parse(Parser.Expr, " (1) ") - .Should().Be(new ParenExpression(new NumberLiteral(1.0))); - + } + [Test] - public void ParenExpr_Nested() => Parse(Parser.Expr, " ((1)) ") - .Should().Be(new ParenExpression(new ParenExpression(new NumberLiteral(1.0)))); + public void ParenExpr_Simple() + { + var input = " (1) "; + var parsed = Parse(Parser.Expr, input) + .Should().Be( + new ParenExpression( + new NumberLiteral(1.0, new TextSpan(input, new Position(2, 0, 0), 1)), + new TextSpan(input, new Position(1, 0, 0), 3) + ) + ); + } [Test] - public void FunctionCall_Empty() => Parse(Parser.FunctionCall, "time ()") - .Should().Be(new FunctionCall("time", ImmutableArray.Empty)); + public void ParenExpr_Nested() + { + var input = "((1))"; + var parsed = Parse(Parser.Expr, input) + .Should().Be( + new ParenExpression( + new ParenExpression( + new NumberLiteral(1.0, new TextSpan(input, new Position(2, 0, 0), 1)), + new TextSpan(input, new Position(1, 0, 0), 3) + ), + new TextSpan(input) + ) + ); + } [Test] - public void FunctionCall_InvalidFunction() + public void FunctionCall_Empty() + { + var input = "time ()"; + Parse(Parser.FunctionCall, input) + .Should().Be(new FunctionCall(Functions.Map["time"], ImmutableArray.Empty, new TextSpan(input))); + } + + [Test] + public void FunctionCall_InvalidName() { Assert.Throws(() => Parse(Parser.Expr, "this_doesnt_exist ()")); } + + [Test] + public void FunctionCall_InvalidParameterCount() + { + Assert.Throws(() => Parse(Parser.Expr, "abs(-1, 1)")) + .Message.Contains("expected 1 argument(s)"); + } + + [Test] + public void FunctionCall_InvalidParameterCountVaradic() + { + Assert.Throws(() => Parse(Parser.Expr, "label_join(instant_vector, 'dst_label', 'separator')")) + .Message.Should().Contain("expected at least 4 argument(s)"); + } + + [Test] + [TestCase("hour()")] + [TestCase("hour(one)")] + [TestCase("hour(one, two)")] + [TestCase("label_join(instant_vector, 'dst_label', 'separator', 'one', 'two')")] + public void FunctionCall_Varadic(string query) + { + Parse(Parser.Expr, query).Should().BeOfType(); + } [Test] public void FunctionCall_OneArg() => Parse(Parser.Expr, "abs (1)") .Should().BeEquivalentTo( - new FunctionCall("abs", new Expr[] { new NumberLiteral(1.0) }.ToImmutableArray()) + new FunctionCall(Functions.Map["abs"], new Expr[] { new NumberLiteral(1.0) }.ToImmutableArray()), + // Don't assert over parsed Span positions, will be tedious to specify all positions + cfg => cfg.Excluding(x => x.Name == "Span") ); [Test] - // NOTE: we do not either validate the parameter count or types of functions - public void FunctionCall_MultiArg() => Parse(Parser.Expr, "abs (1, 2)") + // NOTE: we do not validate the types of functions in the parser + public void FunctionCall_MultiArg() => Parse(Parser.Expr, "histogram_quantile (0.9, blah)") .Should().BeEquivalentTo( - new FunctionCall("abs", new Expr[] { new NumberLiteral(1.0), new NumberLiteral(2.0) }.ToImmutableArray()) + new FunctionCall(Functions.Map["histogram_quantile"], new Expr[] { new NumberLiteral(0.9), new VectorSelector(new MetricIdentifier("blah")) }.ToImmutableArray()), + // Don't assert over parsed Span positions, will be tedious to specify all positions + cfg => cfg.Excluding(x => x.Name == "Span") ); [Test] - // NOTE: we do not either validate the parameter count or types of functions public void FunctionCall_SnakeCase() => Parse(Parser.Expr, "absent_over_time (metric_name )") .Should().BeEquivalentTo( - new FunctionCall("absent_over_time", new Expr[] { new VectorSelector(new MetricIdentifier("metric_name")) }.ToImmutableArray()) + new FunctionCall(Functions.Map["absent_over_time"], new Expr[] { new VectorSelector(new MetricIdentifier("metric_name")) }.ToImmutableArray()), + // Don't assert over parsed Span positions, will be tedious to specify all positions + cfg => cfg.Excluding(x => x.Name == "Span") ); [Test] - public void UnaryExpr_Plus() => Parse(Parser.UnaryExpr, "+1") - .Should().Be(new UnaryExpr(Operators.Unary.Add, new NumberLiteral(1.0))); + public void UnaryExpr_Plus() + { + const string input = "+1"; + Parse(Parser.UnaryExpr, input) + .Should().Be( + new UnaryExpr( + Operators.Unary.Add, + new NumberLiteral(1.0, new TextSpan(input, new Position(1, 0 , 0), 1)), + new TextSpan(input) + ) + ); + } [Test] - public void UnaryExpr_Minus() => Parse(Parser.UnaryExpr, "-1") - .Should().Be(new UnaryExpr(Operators.Unary.Sub, new NumberLiteral(1.0))); - + public void UnaryExpr_Minus() + { + const string input = "-1"; + Parse(Parser.UnaryExpr, input) + .Should().Be( + new UnaryExpr( + Operators.Unary.Sub, + new NumberLiteral(1.0, new TextSpan(input, new Position(1, 0 , 0), 1)), + new TextSpan(input) + ) + ); + } + [Test] public void Expr() { - Parse(Parser.Expr, "'a string'").Should().Be(new StringLiteral('\'', "a string")); - Parse(Parser.Expr, "1").Should().Be(new NumberLiteral(1.0)); + Parse(Parser.Expr, "'a string'").Should().BeOfType(); + Parse(Parser.Expr, "1").Should().BeOfType(); Parse(Parser.Expr, "1 + 1").Should().BeOfType(); Parse(Parser.Expr, "sum (some_expr)").Should().BeOfType(); Parse(Parser.Expr, "some_expr[1d:]").Should().BeOfType(); @@ -306,15 +454,15 @@ public void Expr() Parse(Parser.Expr, "some_expr").Should().BeOfType(); Parse(Parser.Expr, "some_expr[1d]").Should().BeOfType(); Parse(Parser.Expr, "+(1)").Should().BeOfType(); - Parse(Parser.Expr, "abs()").Should().BeOfType(); + Parse(Parser.Expr, "abs(-1)").Should().BeOfType(); } [Test] public void Expr_NumberWithSign() { // Make sure we don't parse this as a unary expression! - Parse(Parser.Expr, "+1").Should().Be(new NumberLiteral(1)); - Parse(Parser.Expr, "-1").Should().Be(new NumberLiteral(-1)); + Parse(Parser.Expr, "+1").Should().Be(new NumberLiteral(1, new TextSpan("+1"))); + Parse(Parser.Expr, "-1").Should().Be(new NumberLiteral(-1, new TextSpan("-1"))); } [Test] @@ -338,7 +486,9 @@ public void Expr_NameKeywordWorkaround() new VectorSelector(new LabelMatchers(new [] { new LabelMatcher("__name__", Operators.LabelMatch.Equal, new StringLiteral('\'', "on")) - }.ToImmutableArray())) + }.ToImmutableArray())), + // Don't assert over parsed Span positions, will be tedious to specify all positions + cfg => cfg.Excluding(x => x.Name == "Span") ); } @@ -352,9 +502,9 @@ public void Expr_Complex() Parse(Parser.Expr, toParse).Should().BeEquivalentTo( new BinaryExpr( new AggregateExpr( - "sum", + Operators.Aggregates["sum"], new FunctionCall( - "rate", + Functions.Map["rate"], new Expr[]{ new MatrixSelector( new VectorSelector(new MetricIdentifier("node_cpu_seconds_total")), @@ -367,9 +517,9 @@ public void Expr_Complex() false ), new AggregateExpr( - "sum", + Operators.Aggregates["sum"], new FunctionCall( - "rate", + Functions.Map["rate"], new Expr[]{ new MatrixSelector( new VectorSelector(new MetricIdentifier("node_cpu_seconds_total")), @@ -389,7 +539,9 @@ public void Expr_Complex() ImmutableArray.Empty, false ) - ) + ), + // Don't assert over parsed Span positions, will be tedious to specify all positions + cfg => cfg.Excluding(x => x.Name == "Span") ); } @@ -412,7 +564,7 @@ public void Invalid_DurationExpr() [Test] public void ParseExpression_With_Comments() { - Parser.ParseExpression(" #some comment \n 1 \n # another comment!").Should().Be(new NumberLiteral(1.0)); + Parser.ParseExpression(" #some comment \n 1 \n # another comment!").Should().BeOfType(); } [Test] @@ -423,85 +575,131 @@ public void GroupingLabels_Nothing() [Test] public void GroupingLabels_Empty() => Parse(Parser.GroupingLabels, " ( )") - .Should().BeEmpty(); + .Value.Should().BeEmpty(); [Test] public void GroupingLabels_One() => Parse(Parser.GroupingLabels, " ( one ) ") - .Should().Equal("one"); - + .Value.Should().Equal("one"); + [Test] - public void GroupingLabels_Many() => Parse(Parser.GroupingLabels, " ( one, two, three ) ") - .Should().Equal("one", "two", "three"); + public void GroupingLabels_Many() + { + var parsed = Parse(Parser.GroupingLabels, " ( one, two, three ) "); + parsed.Value.Should().Equal("one", "two", "three"); + parsed.Span.ToString().Should().Be("( one, two, three )"); + } [Test] - public void VectorMatching_Bool() => Parse(Parser.VectorMatching, "bool") - .Should().BeEquivalentTo(new VectorMatching( - Operators.VectorMatchCardinality.OneToOne, - ImmutableArray.Empty, - false, - ImmutableArray.Empty, - true) - ); - + public void VectorMatching_Bool() + { + const string input = "bool"; + Parse(Parser.VectorMatching, input) + .Should().BeEquivalentTo(new VectorMatching( + Operators.VectorMatchCardinality.OneToOne, + ImmutableArray.Empty, + false, + ImmutableArray.Empty, + true, + new TextSpan(input)) + ); + } + [Test] - public void VectorMatching_Ignoring() => Parse(Parser.VectorMatching, "ignoring (one, two)") - .Should().BeEquivalentTo(new VectorMatching( - Operators.VectorMatchCardinality.OneToOne, - new [] { "one", "two" }.ToImmutableArray(), - false, - ImmutableArray.Empty, - false) - ); + public void VectorMatching_Ignoring() + { + const string input = "ignoring (one, two)"; + + Parse(Parser.VectorMatching, input) + .Should().BeEquivalentTo(new VectorMatching( + Operators.VectorMatchCardinality.OneToOne, + new[] {"one", "two"}.ToImmutableArray(), + false, + ImmutableArray.Empty, + false, + new TextSpan(input)) + ); + } [Test] - public void VectorMatching_On() => Parse(Parser.VectorMatching, "on ()") - .Should().BeEquivalentTo(new VectorMatching( - Operators.VectorMatchCardinality.OneToOne, - ImmutableArray.Empty, - true, - ImmutableArray.Empty, - false) - ); - + public void VectorMatching_On() + { + const string input = "on ()"; + + Parse(Parser.VectorMatching, input) + .Should().BeEquivalentTo(new VectorMatching( + Operators.VectorMatchCardinality.OneToOne, + ImmutableArray.Empty, + true, + ImmutableArray.Empty, + false, + new TextSpan(input)) + ); + } + [Test] - public void VectorMatching_Bool_On() => Parse(Parser.VectorMatching, "bool on ()") - .Should().BeEquivalentTo(new VectorMatching( - Operators.VectorMatchCardinality.OneToOne, - ImmutableArray.Empty, - true, - ImmutableArray.Empty, - true) - ); - + public void VectorMatching_Bool_On() + { + const string input = "bool on ()"; + Parse(Parser.VectorMatching, input) + .Should().BeEquivalentTo(new VectorMatching( + Operators.VectorMatchCardinality.OneToOne, + ImmutableArray.Empty, + true, + ImmutableArray.Empty, + true, + new TextSpan(input) + ) + ); + } + [Test] - public void VectorMatching_GroupLeft() => Parse(Parser.VectorMatching, "on () group_left ()") - .Should().BeEquivalentTo(new VectorMatching( - Operators.VectorMatchCardinality.ManyToOne, - ImmutableArray.Empty, - true, - ImmutableArray.Empty, - false) - ); - + public void VectorMatching_GroupLeft() + { + const string input = "on () group_left ()"; + Parse(Parser.VectorMatching, input) + .Should().BeEquivalentTo(new VectorMatching( + Operators.VectorMatchCardinality.ManyToOne, + ImmutableArray.Empty, + true, + ImmutableArray.Empty, + false, + new TextSpan(input) + ) + ); + } + [Test] - public void VectorMatching_GroupLeftEmpty() => Parse(Parser.VectorMatching, "on () group_left") - .Should().BeEquivalentTo(new VectorMatching( - Operators.VectorMatchCardinality.ManyToOne, - ImmutableArray.Empty, - true, - ImmutableArray.Empty, - false) - ); - + public void VectorMatching_GroupLeftEmpty() + { + const string input = "on () group_left"; + + Parse(Parser.VectorMatching, input) + .Should().BeEquivalentTo(new VectorMatching( + Operators.VectorMatchCardinality.ManyToOne, + ImmutableArray.Empty, + true, + ImmutableArray.Empty, + false, + new TextSpan(input)) + ); + } + [Test] - public void VectorMatching_GroupRight() => Parse(Parser.VectorMatching, "on () group_right (one, two)") - .Should().BeEquivalentTo(new VectorMatching( - Operators.VectorMatchCardinality.OneToMany, - ImmutableArray.Empty, - true, - new []{ "one","two"}.ToImmutableArray(), - false) - ); + public void VectorMatching_GroupRight() + { + const string input = "on () group_right (one, two)"; + Parse(Parser.VectorMatching, input) + .Should().BeEquivalentTo( + new VectorMatching( + Operators.VectorMatchCardinality.OneToMany, + ImmutableArray.Empty, + true, + new []{ "one","two"}.ToImmutableArray(), + false, + new TextSpan(input) + ) + ); + } [Test] [TestCase("group_left ()")] @@ -529,7 +727,9 @@ public void VectorMatching_GroupInvalid(string input) [TestCase("1 unless 1", Operators.Binary.Unless)] public void BinaryExpr_Operators(string input, Operators.Binary expected) { - Parse(Parser.BinaryExpr, input).As().Operator.Should().Be(expected); + var parsed = Parse(Parser.BinaryExpr, input).As(); + parsed.Operator.Should().Be(expected); + parsed.Span.Should().Be(new TextSpan(input)); } [Test] @@ -544,18 +744,56 @@ public void BinaryExpr_SimpleVector() }.ToImmutableArray())), Operators.Binary.Add, new VectorMatching() - )); + ), + // Don't assert over parsed Span positions, will be tedious to specify all positions + cfg => cfg.Excluding(x => x.Name == "Span") + ); } [Test] - public void BinaryExpr_Repetitive() + [TestCase("1 + 2 + 3")] + [TestCase("1 / 2 * 3")] + [TestCase("1 + 2 - 3")] + [TestCase("1 < 2 > 3")] + public void BinaryExpr_PrecedenceEqual(string input) + { + var result = Parse(Parser.BinaryExpr, input) as BinaryExpr; + result.RightHandSide.Should().Be(new NumberLiteral(3.0, new TextSpan(input, new Position(8, 0, 0), 1))); + var binExpr = result.LeftHandSide.Should().BeOfType(); + + binExpr.Which.LeftHandSide.Should().Be(new NumberLiteral(1.0, new TextSpan(input, new Position(0, 0, 0), 1))); + binExpr.Which.RightHandSide.Should().Be(new NumberLiteral(2.0, new TextSpan(input, new Position(4, 0, 0), 1))); + } + + [Test] + [TestCase("1 * 2 + 3")] + [TestCase("1 - 2 > 3")] + [TestCase("1 < 2 and 3")] + [TestCase("1 and 2 or 3")] + public void BinaryExpr_PrecedenceHigher(string input) { - var result = Parse(Parser.BinaryExpr, "1 + 2 + 3") as BinaryExpr; - result.LeftHandSide.Should().Be(new NumberLiteral(1.0)); + var result = Parse(Parser.BinaryExpr, input); + + var binExpr = result.LeftHandSide.Should().BeOfType(); + binExpr.Which.LeftHandSide.Should().BeOfType().Which.Value.Should().Be(1); + binExpr.Which.RightHandSide.Should().BeOfType().Which.Value.Should().Be(2); + + result.RightHandSide.Should().BeOfType().Which.Value.Should().Be(3); + } + + [Test] + [TestCase("1 - 2 / 3")] + [TestCase("1 <= 2 + 3")] + [TestCase("1 unless 2 >= 3")] + [TestCase("1 or 2 unless 3")] + public void BinaryExpr_PrecedenceLower(string input) + { + var result = Parse(Parser.BinaryExpr, input); + result.LeftHandSide.Should().BeOfType().Which.Value.Should().Be(1); var binExpr = result.RightHandSide.Should().BeOfType(); - binExpr.Which.LeftHandSide.Should().Be(new NumberLiteral(2.0)); - binExpr.Which.RightHandSide.Should().Be(new NumberLiteral(3.0)); + binExpr.Which.LeftHandSide.Should().BeOfType().Which.Value.Should().Be(2); + binExpr.Which.RightHandSide.Should().BeOfType().Which.Value.Should().Be(3); } [Test] @@ -566,78 +804,137 @@ public void BinaryExpr_Nested() .Expr.Should().BeOfType().Which .LeftHandSide.Should().BeOfType().Which .Expr.Should().BeOfType().Which - .LeftHandSide.Should().Be(new NumberLiteral(1.0)); - - Parse(Parser.Expr, "(100 * (1) / 128)"); + .LeftHandSide.Should().BeOfType().Subject.Value.Should().Be(1); } [Test] public void BinaryExpr_VectorMatching() { - Parse(Parser.BinaryExpr, "1 + bool 2") + Parse(Parser.BinaryExpr, "1 > bool 2") .Should().BeEquivalentTo(new BinaryExpr( - new NumberLiteral(1.0), - new NumberLiteral(2), - Operators.Binary.Add, - new VectorMatching(true) - )); + new NumberLiteral(1.0), + new NumberLiteral(2), + Operators.Binary.Gtr, + new VectorMatching(true) + ), + // Don't assert over parsed Span positions, will be tedious to specify all positions + cfg => cfg.Excluding(x => x.Name == "Span") + ); + } + + [Test] + [TestCase("1 + bool 2")] + [TestCase("1 - bool 2")] + [TestCase("1 / bool 2")] + [TestCase("1 * bool 2")] + [TestCase("1 or bool 2")] + [TestCase("1 unless bool 2")] + [TestCase("1 and bool 2")] + public void BinaryExpr_BoolNonComparison(string query) + { + Assert.Throws(() => Parse(Parser.BinaryExpr, "1 + bool 2")) + .Message.Should().Contain("bool modifier can only be used on comparison operators"); } [Test] [TestCase("avg (blah)", "avg")] - [TestCase("bottomk (blah)", "bottomk")] + [TestCase("bottomk (2, blah)", "bottomk")] [TestCase("count (blah)", "count")] - [TestCase("count_values (blah)", "count_values")] + [TestCase("count_values (blah, values)", "count_values")] [TestCase("group (blah)", "group")] [TestCase("max (blah)", "max")] [TestCase("min (blah)", "min")] - [TestCase("quantile (blah)", "quantile")] + [TestCase("quantile (0.5, blah)", "quantile")] [TestCase("stddev (blah)", "stddev")] [TestCase("stdvar (blah)", "stdvar")] [TestCase("sum (blah)", "sum")] - [TestCase("topk (blah)", "topk")] + [TestCase("topk (1, blah)", "topk")] public void AggregateExpr_Operator(string input, string expected) => Parse(Parser.AggregateExpr, input) - .OperatorName.Should().Be(expected); + .Operator.Name.Should().Be(expected); [Test] - public void AggregateExpr_NoMod() => Parse(Parser.AggregateExpr, "sum (blah)").Should().BeEquivalentTo( - new AggregateExpr( - "sum", - new VectorSelector(new MetricIdentifier("blah")), - null, - ImmutableArray.Empty, - false - )); - + public void AggregateExpr_NoMod() + { + const string input = "sum (blah)"; + + var result = Parse(Parser.AggregateExpr, input); + result.Should().BeEquivalentTo( + new AggregateExpr( + Operators.Aggregates["sum"], + new VectorSelector(new MetricIdentifier("blah")), + null, + ImmutableArray.Empty, + false + ), + // Don't assert over parsed Span positions, will be tedious to specify all positions + cfg => cfg.Excluding(x => x.Name == "Span") + ); + + result.Span.Should().Be(new TextSpan(input)); + } + [Test] - public void AggregateExpr_LeadingModBy() => Parse(Parser.AggregateExpr, "sum by (one, two) (blah)").Should().BeEquivalentTo( - new AggregateExpr( - "sum", - new VectorSelector(new MetricIdentifier("blah")), - null, - new string[] {"one", "two"}.ToImmutableArray(), - false - )); - + public void AggregateExpr_LeadingModBy() + { + const string input = "sum by (one, two) (blah)"; + var result = Parse(Parser.AggregateExpr, input); + + result.Should().BeEquivalentTo( + new AggregateExpr( + Operators.Aggregates["sum"], + new VectorSelector(new MetricIdentifier("blah")), + null, + new string[] {"one", "two"}.ToImmutableArray(), + false + ), + // Don't assert over parsed Span positions, will be tedious to specify all positions + cfg => cfg.Excluding(x => x.Name == "Span") + ); + result.Span.Should().Be(new TextSpan(input)); + } + [Test] - public void AggregateExpr_TrailingModWithout() => Parse(Parser.AggregateExpr, "sum (blah) without (one)").Should().BeEquivalentTo( - new AggregateExpr( - "sum", - new VectorSelector(new MetricIdentifier("blah")), - null, - new string[] {"one" }.ToImmutableArray(), - true - )); - + public void AggregateExpr_TrailingModWithout() + { + const string input = "sum (blah) without (one)"; + + var result = Parse(Parser.AggregateExpr, input); + result.Should().BeEquivalentTo( + new AggregateExpr( + Operators.Aggregates["sum"], + new VectorSelector(new MetricIdentifier("blah")), + null, + new string[] {"one"}.ToImmutableArray(), + true + ), + // Don't assert over parsed Span positions, will be tedious to specify all positions + cfg => cfg.Excluding(x => x.Name == "Span") + ); + result.Span.Should().Be(new TextSpan(input)); + } + [Test] public void AggregateExpr_TwoArgs() => Parse(Parser.AggregateExpr, "quantile (0.5, blah)").Should().BeEquivalentTo( new AggregateExpr( - "quantile", + Operators.Aggregates["quantile"], new VectorSelector(new MetricIdentifier("blah")), new NumberLiteral(0.5), ImmutableArray.Empty, false - )); + ), + // Don't assert over parsed Span positions, will be tedious to specify all positions + cfg => cfg.Excluding(x => x.Name == "Span") + ); + + [Test] + public void AggregateExpr_OneArgs_InvalidFunc() => + Assert.Throws(() => Parse(Parser.AggregateExpr, "quantile (blah)")) + .Message.Should().Contain("wrong number of arguments for aggregate expression provided, expected 2, got 1"); + + [Test] + public void AggregateExpr_TwoArgs_InvalidFunc() => + Assert.Throws(() => Parse(Parser.AggregateExpr, "sum (0.5, blah)")) + .Message.Should().Contain("wrong number of arguments for aggregate expression provided, expected 1, got 2"); [Test] public void AggregateExpr_LabelNameAggOp() @@ -646,38 +943,58 @@ public void AggregateExpr_LabelNameAggOp() } [Test] - public void Subquery_WithStep() => Parse(Parser.Expr, "blah[1h:1m]").Should().BeEquivalentTo( - new SubqueryExpr( - new VectorSelector(new MetricIdentifier("blah")), - new Duration(TimeSpan.FromHours(1)), - new Duration(TimeSpan.FromMinutes(1)) - )); - - [Test] - public void Subquery_WithoutStep() => Parse(Parser.Expr, "blah[1d:]").Should().BeEquivalentTo( - new SubqueryExpr( - new VectorSelector(new MetricIdentifier("blah")), - new Duration(TimeSpan.FromDays(1)), - null - )); - - [Test] - public void Subquery_Expressions() - { - Parse(Parser.Expr, "vector(1) [1h:]").Should().BeOfType().Which + public void Subquery_WithStep() + { + const string input = "blah[1h:1m]"; + var result = Parse(Parser.Expr, input); + + result.Should().BeEquivalentTo( + new SubqueryExpr( + new VectorSelector(new MetricIdentifier("blah")), + new Duration(TimeSpan.FromHours(1)), + new Duration(TimeSpan.FromMinutes(1)) + ), + // Don't assert over parsed Span positions, will be tedious to specify all positions + cfg => cfg.Excluding(x => x.Name == "Span") + ); + result.Span.Should().Be(new TextSpan(input)); + } + + [Test] + public void Subquery_WithoutStep() + { + const string input = "blah[1d:]"; + var result = Parse(Parser.Expr, input); + + result.Should().BeEquivalentTo( + new SubqueryExpr( + new VectorSelector(new MetricIdentifier("blah")), + new Duration(TimeSpan.FromDays(1)), + null + ), + // Don't assert over parsed Span positions, will be tedious to specify all positions + cfg => cfg.Excluding(x => x.Name == "Span") + ); + result.Span.Should().Be(new TextSpan(input)); + } + + [Test] + public void Subquery_Expressions() + { + Parse(Parser.Expr, "vector(1) [1h:]").Should().BeOfType().Which .Expr.Should().BeOfType(); - - Parse(Parser.Expr, "1 + 1 [1h:]").Should().BeOfType().Which + + Parse(Parser.Expr, "1 + 1 [1h:]").Should().BeOfType().Which .RightHandSide.Should().BeOfType(); - - Parse(Parser.Expr, "(1 + 1) [1h:]").Should().BeOfType().Which + + Parse(Parser.Expr, "(1 + 1) [1h:]").Should().BeOfType().Which .Expr.Should().BeOfType(); - - Parse(Parser.Expr, "blah{} [1h:]").Should().BeOfType().Which - .Expr.Should().BeOfType(); - } - public static T Parse(TokenListParser parser, string input) + Parse(Parser.Expr, "blah{} [1h:]").Should().BeOfType().Which + .Expr.Should().BeOfType(); + } + + public static T Parse(TokenListParser parser, string input) { var tokens = new Tokenizer().Tokenize(input); try diff --git a/tests/PromQL.Parser.Tests/PrinterTests.cs b/tests/PromQL.Parser.Tests/PrinterTests.cs index f3015c1..8d8927e 100644 --- a/tests/PromQL.Parser.Tests/PrinterTests.cs +++ b/tests/PromQL.Parser.Tests/PrinterTests.cs @@ -54,7 +54,7 @@ public void NumberLiteral_Inf_ToPromQl() [Test] public void Aggregate_ToPromQl() { - _printer.ToPromQl(new AggregateExpr("sum", + _printer.ToPromQl(new AggregateExpr(Operators.Aggregates["sum"], new VectorSelector(new MetricIdentifier("test_expr")), null, ImmutableArray.Empty, @@ -65,7 +65,7 @@ public void Aggregate_ToPromQl() [Test] public void Aggregate_WithLabels_ToPromQl() { - _printer.ToPromQl(new AggregateExpr("sum", + _printer.ToPromQl(new AggregateExpr(Operators.Aggregates["sum"], new VectorSelector(new MetricIdentifier("test_expr")), null, new [] { "one" }.ToImmutableArray(), @@ -76,7 +76,7 @@ public void Aggregate_WithLabels_ToPromQl() [Test] public void Aggregate_WithParam_ToPromQl() { - _printer.ToPromQl(new AggregateExpr("quantile", + _printer.ToPromQl(new AggregateExpr(Operators.Aggregates["quantile"], new VectorSelector(new MetricIdentifier("test_expr")), new NumberLiteral(1), new[] {"label1", "label2"}.ToImmutableArray(), @@ -181,13 +181,13 @@ public void Complex_ToPromQl() => ), new UnaryExpr( Operators.Unary.Sub, - new FunctionCall("sum", new Expr[] + new FunctionCall(Functions.Map["vector"], new Expr[] { new OffsetExpr(new VectorSelector(new MetricIdentifier("this_is_a_metric")), new Duration(TimeSpan.FromMinutes(5))) }.ToImmutableArray()) ), Operators.Binary.Add, null - )).Should().Be("(another_metric{one='test', two!='test2'}[1h][1d:5m]) + -sum(this_is_a_metric offset 5m)"); + )).Should().Be("(another_metric{one='test', two!='test2'}[1h][1d:5m]) + -vector(this_is_a_metric offset 5m)"); } } \ No newline at end of file diff --git a/tests/PromQL.Parser.Tests/TypeCheckerTests.cs b/tests/PromQL.Parser.Tests/TypeCheckerTests.cs new file mode 100644 index 0000000..f8aa0ad --- /dev/null +++ b/tests/PromQL.Parser.Tests/TypeCheckerTests.cs @@ -0,0 +1,192 @@ +using System; +using FluentAssertions; +using NUnit.Framework; + +namespace PromQL.Parser.Tests +{ + [TestFixture] + public class TypeCheckerTests + { + [Test] + public void Single_Expected_Type_Not_Found() + { + Assert.Throws(() => Parser.ParseExpression("sum_over_time(instant_vector)").CheckType()) + .Message.Should().Be("Unexpected type 'instant vector' was provided, expected range vector: 14 (line 1, column 15)"); + } + + [Test] + public void Multiple_Expected_Types_Not_Found() + { + Assert.Throws(() => Parser.ParseExpression("+'hello'").CheckType()) + .Message.Should().Be("Unexpected type 'string' was provided, expected one of 'scalar', 'instant vector': 1 (line 1, column 2)"); + } + + [Test] + [TestCase("sum_over_time(instant_vector)")] + [TestCase("hour(range_vector[1h])")] + public void FunctionCall_InvalidArgumentTypes(string expr) + { + Assert.Throws(() => Parser.ParseExpression(expr).CheckType()); + } + + [Test] + [TestCase("sum_over_time(range_vector[1d])", ValueType.Vector)] + [TestCase("hour()", ValueType.Vector)] + [TestCase("hour(instant_vector)", ValueType.Vector)] + [TestCase("hour(instant_vector, instant_vector2, instant_vector3)", ValueType.Vector)] + public void FunctionCall_ValidArgumentTypes(string expr, ValueType expected) + { + Parser.ParseExpression(expr).CheckType() + .Should().Be(expected); + } + + [Test] + [TestCase("-'a'")] + [TestCase("-some_matrix[1d]")] + public void UnaryExpr_InvalidArgumentTypes(string expr) + { + Assert.Throws(() => Parser.ParseExpression(expr).CheckType()); + } + + [Test] + [TestCase("-1", ValueType.Scalar)] + [TestCase("-some_vector", ValueType.Vector)] + public void UnaryExpr_ValidArgumentTypes(string expr, ValueType expected) + { + Parser.ParseExpression(expr).CheckType() + .Should().Be(expected); + } + + [Test] + [TestCase("some_matrix[1d][1d:1h]")] + [TestCase("'string'[1d:1h]")] + [TestCase("1[1d:1h]")] + [TestCase("time()[1d:1h]")] + public void SubqueryExpr_InvalidArgumentTypes(string expr) + { + Assert.Throws(() => Parser.ParseExpression(expr).CheckType()); + } + + [Test] + [TestCase("some_vector[1d:1h]", ValueType.Matrix)] + [TestCase("hour()[1d:1h]", ValueType.Matrix)] + public void SubqueryExpr_ValidArgumentTypes(string expr, ValueType expected) + { + Parser.ParseExpression(expr).CheckType() + .Should().Be(expected); + } + + [Test] + public void BinaryExpr_InvalidLhsString() + { + Assert.Throws( + () => Parser.ParseExpression("'a' + 1 + 1 + 1").CheckType() + ) + .Message.Should().Contain("Unexpected type 'string' was provided, expected one of 'scalar', 'instant vector'"); + } + + [Test] + public void BinaryExpr_InvalidRhsString() + { + Assert.Throws( + () => Parser.ParseExpression("1 + 1 + 1 + 'a'").CheckType() + ) + .Message.Should().Contain("Unexpected type 'string' was provided, expected one of 'scalar', 'instant vector'"); + } + + [Test] + public void BinaryExpr_InvalidLhsMatrix() + { + Assert.Throws( + () => Parser.ParseExpression("some_matrix[1d] + 1 + 1 + 1").CheckType() + ) + .Message.Should().Contain("Unexpected type 'range vector' was provided, expected one of 'scalar', 'instant vector'"); + } + + [Test] + public void BinaryExpr_InvalidRhsMatrix() + { + Assert.Throws( + () => Parser.ParseExpression("1 + 1 + 1 + some_matrix[1d]").CheckType() + ) + .Message.Should().Contain("Unexpected type 'range vector' was provided, expected one of 'scalar', 'instant vector'"); + } + + [Test] + public void BinaryExpr_Scalar() + { + Parser.ParseExpression("1 > bool 1").CheckType().Should().Be(ValueType.Scalar); + } + + [Test] + public void BinaryExpr_Vector() + { + Parser.ParseExpression("first_vector and second_vector").CheckType().Should().Be(ValueType.Vector); + } + + [Test] + public void BinaryExpr_VectorScalar() + { + Parser.ParseExpression("first_vector > 1").CheckType().Should().Be(ValueType.Vector); + } + + [Test] + public void BinaryExpr_VectorScalarSet() + { + Assert.Throws( + () => Parser.ParseExpression("first_vector and 1").CheckType() + ) + .Message.Should().Contain("set operator And not allowed in binary scalar expression"); + } + + [Test] + public void BinaryExpr_ScalarNoBool() + { + Assert.Throws( + () => Parser.ParseExpression("1 > 1").CheckType() + ).Message.Should().Contain("comparisons between scalars must use bool modifier"); + } + + [Test] + public void BinarExpr_Associativity() + { + Parser.ParseExpression("up == 1 unless on (instance) (test)").CheckType() + .Should().Be(ValueType.Vector); + } + + [Test] + public void BinarExpr_Precedence() + { + Console.WriteLine(Parser.ParseExpression(@"(metric1) > 80 and 100 - (metric2) < 90")); + Parser.ParseExpression(@"(metric1) > 80 and 100 - (metric2) < 90").CheckType() + .Should().Be(ValueType.Vector); + } + + [Test] + public void ParenExpression_Nested() + { + Assert.Throws( + () => Parser.ParseExpression("((((-'a'))))").CheckType() + ) + .Message.Should().Be("Unexpected type 'string' was provided, expected one of 'scalar', 'instant vector': 5 (line 1, column 6)"); + } + + [Test] + [TestCase("sum('one')", "'string' was provided, expected instant vector")] + [TestCase("avg(1)", "'scalar' was provided, expected instant vector")] + [TestCase("max(scalar(1))", "'scalar' was provided, expected instant vector")] + [TestCase("min(blah[2m])", "'range vector' was provided, expected instant vector")] + [TestCase("quantile('1', test)", "'string' was provided, expected scalar")] + [TestCase("quantile(1, test[1m])", "'range vector' was provided, expected instant vector")] + [TestCase("topk(2, test[1m])", "'range vector' was provided, expected instant vector")] + [TestCase("bottomk(one, '2')", "'instant vector' was provided, expected scalar")] + [TestCase("count_values(one, 'one')", "'instant vector' was provided, expected string")] + public void AggregateExpression_InvalidParameterTypes(string input, string expected) + { + Assert.Throws( + () => Parser.ParseExpression(input).CheckType() + ) + .Message.Should().Contain(expected); + } + } +} \ No newline at end of file