From 483db9310d036473055788fe491bc604091ac597 Mon Sep 17 00:00:00 2001 From: Julien Pivotto <291750+roidelapluie@users.noreply.github.com> Date: Wed, 6 May 2026 12:34:55 +0200 Subject: [PATCH 1/2] promql/parser: recognize range in duration expressions Signed-off-by: Julien Pivotto <291750+roidelapluie@users.noreply.github.com> --- promql/parser/lex.go | 2 +- promql/parser/parse_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/promql/parser/lex.go b/promql/parser/lex.go index 13b101e783..547b3f2a24 100644 --- a/promql/parser/lex.go +++ b/promql/parser/lex.go @@ -507,7 +507,7 @@ func lexStatements(l *Lexer) stateFn { l.emit(COLON) l.gotColon = true return lexStatements - case 's', 'S', 'm', 'M': + case 's', 'S', 'm', 'M', 'r', 'R': if l.scanDurationKeyword() { return lexStatements } diff --git a/promql/parser/parse_test.go b/promql/parser/parse_test.go index 482952ee64..b3ea1bb0f4 100644 --- a/promql/parser/parse_test.go +++ b/promql/parser/parse_test.go @@ -4718,6 +4718,32 @@ var testExpr = []struct { EndPos: 12, }, }, + { + input: `foo[2m/range()]`, + expected: &MatrixSelector{ + VectorSelector: &VectorSelector{ + Name: "foo", + LabelMatchers: []*labels.Matcher{ + MustLabelMatcher(labels.MatchEqual, model.MetricNameLabel, "foo"), + }, + PosRange: posrange.PositionRange{Start: 0, End: 3}, + }, + RangeExpr: &DurationExpr{ + Op: DIV, + LHS: &NumberLiteral{ + Val: 120, + Duration: true, + PosRange: posrange.PositionRange{Start: 4, End: 6}, + }, + RHS: &DurationExpr{ + Op: RANGE, + StartPos: 7, + EndPos: 14, + }, + }, + EndPos: 15, + }, + }, { input: `foo[-range()]`, expected: &MatrixSelector{ From 8739d37781b76f4f163e93ec24c6b4c51d59947b Mon Sep 17 00:00:00 2001 From: Julien Pivotto <291750+roidelapluie@users.noreply.github.com> Date: Wed, 6 May 2026 13:16:56 +0200 Subject: [PATCH 2/2] promql/parser: use map-based dispatch for duration keyword lexing Replace the hardcoded switch over start characters and keyword names with a map-driven approach. durationKeywordTokens maps lowercase keyword strings to their token types, and isDurationKeywordStartChar derives the valid start characters from that map, so adding a new duration keyword only requires one change instead of three. Signed-off-by: Julien Pivotto <291750+roidelapluie@users.noreply.github.com> --- promql/parser/lex.go | 54 ++++++++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/promql/parser/lex.go b/promql/parser/lex.go index 547b3f2a24..99620cacac 100644 --- a/promql/parser/lex.go +++ b/promql/parser/lex.go @@ -499,15 +499,15 @@ func lexStatements(l *Lexer) stateFn { l.backup() return lexKeywordOrIdentifier } - switch r { - case ':': + switch { + case r == ':': if l.gotColon { return l.errorf("unexpected colon %q", r) } l.emit(COLON) l.gotColon = true return lexStatements - case 's', 'S', 'm', 'M', 'r', 'R': + case isDurationKeywordStartChar(r): if l.scanDurationKeyword() { return lexStatements } @@ -935,6 +935,32 @@ func lexNumber(l *Lexer) stateFn { return lexStatements } +// durationKeywordTokens maps lowercase duration keyword names to their token types. +var durationKeywordTokens = map[string]ItemType{ + "step": STEP, + "range": RANGE, + "min": MIN, + "max": MAX, +} + +// durationKeywordStartChars is the set of lowercase runes that can start a duration keyword, +// derived from durationKeywordTokens. +var durationKeywordStartChars = makeDurationKeywordStartChars() + +func makeDurationKeywordStartChars() map[rune]struct{} { + m := make(map[rune]struct{}, len(durationKeywordTokens)) + for kw := range durationKeywordTokens { + m[rune(kw[0])] = struct{}{} + } + return m +} + +// isDurationKeywordStartChar reports whether r can be the first character of a duration keyword. +func isDurationKeywordStartChar(r rune) bool { + _, ok := durationKeywordStartChars[unicode.ToLower(r)] + return ok +} + func (l *Lexer) scanDurationKeyword() bool { for { switch r := l.next(); { @@ -942,24 +968,12 @@ func (l *Lexer) scanDurationKeyword() bool { // absorb. default: l.backup() - word := l.input[l.start:l.pos] - kw := strings.ToLower(word) - switch kw { - case "step": - l.emit(STEP) + word := strings.ToLower(l.input[l.start:l.pos]) + if tok, ok := durationKeywordTokens[word]; ok { + l.emit(tok) return true - case "range": - l.emit(RANGE) - return true - case "min": - l.emit(MIN) - return true - case "max": - l.emit(MAX) - return true - default: - return false } + return false } } } @@ -1239,7 +1253,7 @@ func lexDurationExpr(l *Lexer) stateFn { case r == ',': l.emit(COMMA) return lexDurationExpr - case r == 's' || r == 'S' || r == 'm' || r == 'M' || r == 'r' || r == 'R': + case isDurationKeywordStartChar(r): if l.scanDurationKeyword() { return lexDurationExpr }