From 5699439dc37793e1b4c4c4dc73e721a0bb16a1e1 Mon Sep 17 00:00:00 2001 From: Ning Sun Date: Fri, 3 Apr 2026 12:22:14 +0800 Subject: [PATCH 1/9] feat: add function sync script with upstream --- .github/workflows/function-comparison.yml | 19 ++ scripts/compare_functions.py | 277 ++++++++++++++++++++++ src/parser/function.rs | 77 +++++- 3 files changed, 365 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/function-comparison.yml create mode 100755 scripts/compare_functions.py diff --git a/.github/workflows/function-comparison.yml b/.github/workflows/function-comparison.yml new file mode 100644 index 0000000..1a30a41 --- /dev/null +++ b/.github/workflows/function-comparison.yml @@ -0,0 +1,19 @@ +name: Function Comparison + +on: + schedule: + - cron: '0 0 * * 0' + workflow_dispatch: + +jobs: + compare: + name: Compare Functions + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + - name: Run function comparison + run: python scripts/compare_functions.py diff --git a/scripts/compare_functions.py b/scripts/compare_functions.py new file mode 100755 index 0000000..4652320 --- /dev/null +++ b/scripts/compare_functions.py @@ -0,0 +1,277 @@ +#!/usr/bin/env python3 +""" +Script to compare Prometheus Go functions.go with Rust functions.rs +Ensures Rust functions are complete and consistent with Go version. +""" + +import re +import sys +from typing import Dict, List, Optional, Tuple +from dataclasses import dataclass + + +@dataclass +class FunctionDef: + name: str + arg_types: List[str] + variadic: int + return_type: str + experimental: bool + + def __str__(self): + exp_flag = " [EXP]" if self.experimental else "" + return f"{self.name}: args={self.arg_types}, variadic={self.variadic}, return={self.return_type}{exp_flag}" + + +def parse_go_functions(content: str) -> Dict[str, FunctionDef]: + """Parse Prometheus Go functions.go to extract function definitions.""" + functions = {} + + # Find all function entry points + entries = [] + pattern = r'"([^"]+)":\s*\{' + for match in re.finditer(pattern, content): + entries.append((match.start(), match.end(), match.group(1))) + + # Extract each function block + for i, (start, end, name) in enumerate(entries): + block_start = end + # Find the closing brace for this block + brace_count = 1 + block_end = block_start + while brace_count > 0 and block_end < len(content): + block_end += 1 + if content[block_end] == "{": + brace_count += 1 + elif content[block_end] == "}": + brace_count -= 1 + + block_content = content[block_start:block_end] + + # Parse Name field + name_match = re.search(r'Name:\s*"([^"]+)"', block_content) + if not name_match: + continue + + # Parse ArgTypes field + arg_types = [] + arg_types_match = re.search( + r"ArgTypes:\s*\[\]ValueType\{(.*?)\}", block_content, re.DOTALL + ) + if arg_types_match: + arg_types_str = arg_types_match.group(1).strip() + for arg in re.findall(r"ValueType(\w+)", arg_types_str): + arg_types.append(arg) + + # Parse Variadic field + variadic = 0 + variadic_match = re.search(r"Variadic:\s*(-?\d+)", block_content) + if variadic_match: + variadic = int(variadic_match.group(1)) + + # Parse ReturnType field + return_type = "" + return_match = re.search(r"ReturnType:\s*([^,\n}]+)", block_content) + if return_match: + return_type_clean = re.sub(r"ValueType", "", return_match.group(1)).strip() + return_type = return_type_clean + + # Parse Experimental field + experimental = False + experimental_match = re.search(r"Experimental:\s*(true|false)", block_content) + if experimental_match: + experimental = experimental_match.group(1).lower() == "true" + + functions[name] = FunctionDef( + name=name, + arg_types=arg_types, + variadic=variadic, + return_type=return_type, + experimental=experimental, + ) + + return functions + + +def parse_rust_functions(content: str) -> Dict[str, FunctionDef]: + """Parse Rust functions.rs to extract function definitions.""" + functions = {} + + # Pattern to match function! macro calls + # Example: + # function!("abs", vec![ValueType::Vector], 0, ValueType::Vector, false), + # function!("days_in_month", vec![ValueType::Vector], 1, ValueType::Vector, false), + # function!("label_join", vec![ValueType::Vector, ValueType::String, ValueType::String, ValueType::String], -1, ValueType::Vector, false), + # function!("double_exponential_smoothing", vec![ValueType::Matrix, ValueType::Scalar, ValueType::Scalar], 0, ValueType::Vector, true), + pattern = r'function!\(\s*"([^"]+)"\s*,\s*vec!\[(.*?)\]\s*,\s*(-?\d+)\s*,\s*ValueType::(\w+)\s*,\s*(true|false)\s*\),' + + for match in re.finditer(pattern, content, re.DOTALL): + name = match.group(1) + arg_types_str = match.group(2).strip() + variadic = int(match.group(3)) + return_type = match.group(4) + experimental = match.group(5).lower() == "true" + + # Parse arg types + arg_types = [] + if arg_types_str: + for arg in re.findall(r"ValueType::(\w+)", arg_types_str): + arg_types.append(arg) + + functions[name] = FunctionDef( + name=name, + arg_types=arg_types, + variadic=variadic, + return_type=return_type, + experimental=experimental, + ) + + return functions + + +def normalize_type(type_str: str) -> str: + """Normalize type names for comparison.""" + # Map Go types to Rust types + type_mapping = { + "String": "String", + "None": "None", + } + return type_mapping.get(type_str, type_str) + + +def compare_functions(go_func: FunctionDef, rust_func: FunctionDef) -> List[str]: + """Compare two function definitions and return list of differences.""" + differences = [] + + # Compare arg types + go_args = [normalize_type(t) for t in go_func.arg_types] + rust_args = [normalize_type(t) for t in rust_func.arg_types] + + if go_args != rust_args: + differences.append(f" Arg types differ: Go={go_args}, Rust={rust_args}") + + # Compare variadic + if go_func.variadic != rust_func.variadic: + differences.append( + f" Variadic differs: Go={go_func.variadic}, Rust={rust_func.variadic}" + ) + + # Compare return type + go_return = normalize_type(go_func.return_type) + rust_return = normalize_type(rust_func.return_type) + if go_return != rust_return: + differences.append(f" Return type differs: Go={go_return}, Rust={rust_return}") + + # Compare experimental flag + if go_func.experimental != rust_func.experimental: + differences.append( + f" Experimental flag differs: Go={go_func.experimental}, Rust={rust_func.experimental}" + ) + + return differences + + +def main(): + import subprocess + + # Fetch Prometheus Go functions.go from GitHub + go_url = "https://raw.githubusercontent.com/prometheus/prometheus/main/promql/parser/functions.go" + print(f"Fetching Prometheus functions.go from {go_url}...") + + try: + result = subprocess.run( + ["curl", "-s", go_url], capture_output=True, text=True, check=True + ) + go_content = result.stdout + except Exception as e: + print(f"Error fetching Go file: {e}") + sys.exit(1) + + # Read Rust functions.rs + rust_file = "src/parser/function.rs" + print(f"Reading Rust functions from {rust_file}...") + + try: + with open(rust_file, "r") as f: + rust_content = f.read() + except Exception as e: + print(f"Error reading Rust file: {e}") + sys.exit(1) + + # Parse both files + go_functions = parse_go_functions(go_content) + rust_functions = parse_rust_functions(rust_content) + + print(f"\nParsed {len(go_functions)} functions from Go") + print(f"Parsed {len(rust_functions)} functions from Rust\n") + + # Find missing functions in Rust + missing_in_rust = set(go_functions.keys()) - set(rust_functions.keys()) + + # Find extra functions in Rust + extra_in_rust = set(rust_functions.keys()) - set(go_functions.keys()) + + # Find differences in common functions + common_functions = set(go_functions.keys()) & set(rust_functions.keys()) + differences = {} + + for func_name in sorted(common_functions): + go_func = go_functions[func_name] + rust_func = rust_functions[func_name] + + diff = compare_functions(go_func, rust_func) + if diff: + differences[func_name] = (go_func, rust_func, diff) + + # Print results + print("=" * 80) + print("COMPARISON RESULTS") + print("=" * 80) + + if missing_in_rust: + print(f"\nāŒ {len(missing_in_rust)} function(s) MISSING in Rust:") + for func in sorted(missing_in_rust): + print(f" - {func}") + + if extra_in_rust: + print(f"\nāš ļø {len(extra_in_rust)} function(s) EXTRA in Rust (not in Go):") + for func in sorted(extra_in_rust): + print(f" - {func}") + + if differences: + print(f"\nšŸ” {len(differences)} function(s) have DIFFERENCES:") + for func_name in sorted(differences.keys()): + go_func, rust_func, diff = differences[func_name] + print(f"\n {func_name}:") + print(f" Go version: {go_func}") + print(f" Rust version: {rust_func}") + for d in diff: + print(f" {d}") + + # Summary + print("\n" + "=" * 80) + print("SUMMARY") + print("=" * 80) + + total_go = len(go_functions) + total_rust = len(rust_functions) + total_common = len(common_functions) + total_differences = len(differences) + + print(f"Go functions: {total_go}") + print(f"Rust functions: {total_rust}") + print(f"Common functions: {total_common}") + print(f"Missing in Rust: {len(missing_in_rust)}") + print(f"Extra in Rust: {len(extra_in_rust)}") + print(f"Differences: {total_differences}") + + if not missing_in_rust and not differences: + print("\nāœ… All functions are COMPLETE and CONSISTENT!") + sys.exit(0) + else: + print("\nāŒ Issues found - please review and fix") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/src/parser/function.rs b/src/parser/function.rs index b61a4be..78817ba 100644 --- a/src/parser/function.rs +++ b/src/parser/function.rs @@ -249,28 +249,28 @@ lazy_static! { ), function!("exp", vec![ValueType::Vector], 0, ValueType::Vector, false), function!( - "floor", - vec![ValueType::Vector], + "first_over_time", + vec![ValueType::Matrix], 0, ValueType::Vector, - false + true ), function!( - "histogram_count", + "floor", vec![ValueType::Vector], 0, ValueType::Vector, false ), function!( - "histogram_sum", + "histogram_avg", vec![ValueType::Vector], 0, ValueType::Vector, false ), function!( - "histogram_avg", + "histogram_count", vec![ValueType::Vector], 0, ValueType::Vector, @@ -290,6 +290,18 @@ lazy_static! { ValueType::Vector, false ), + function!( + "histogram_quantiles", + vec![ + ValueType::Vector, + ValueType::String, + ValueType::Scalar, + ValueType::Scalar + ], + 9, + ValueType::Vector, + true + ), function!( "histogram_stddev", vec![ValueType::Vector], @@ -304,6 +316,20 @@ lazy_static! { ValueType::Vector, false ), + function!( + "histogram_sum", + vec![ValueType::Vector], + 0, + ValueType::Vector, + false + ), + function!( + "info", + vec![ValueType::Vector, ValueType::Vector], + 1, + ValueType::Vector, + true + ), function!( "double_exponential_smoothing", vec![ValueType::Matrix, ValueType::Scalar, ValueType::Scalar], @@ -381,6 +407,13 @@ lazy_static! { false ), function!("log2", vec![ValueType::Vector], 0, ValueType::Vector, false), + function!( + "mad_over_time", + vec![ValueType::Matrix], + 0, + ValueType::Vector, + true + ), function!( "max_over_time", vec![ValueType::Matrix], @@ -395,6 +428,34 @@ lazy_static! { ValueType::Vector, false ), + function!( + "ts_of_first_over_time", + vec![ValueType::Matrix], + 0, + ValueType::Vector, + true + ), + function!( + "ts_of_last_over_time", + vec![ValueType::Matrix], + 0, + ValueType::Vector, + true + ), + function!( + "ts_of_max_over_time", + vec![ValueType::Matrix], + 0, + ValueType::Vector, + true + ), + function!( + "ts_of_min_over_time", + vec![ValueType::Matrix], + 0, + ValueType::Vector, + true + ), function!( "minute", vec![ValueType::Vector], @@ -467,14 +528,14 @@ lazy_static! { ), function!( "sort_by_label", - vec![ValueType::Vector, ValueType::String, ValueType::String], + vec![ValueType::Vector, ValueType::String], -1, ValueType::Vector, true ), function!( "sort_by_label_desc", - vec![ValueType::Vector, ValueType::String, ValueType::String], + vec![ValueType::Vector, ValueType::String], -1, ValueType::Vector, true From e0f067dbcb8099dee6c35322b691ef708959ee15 Mon Sep 17 00:00:00 2001 From: Ning Sun Date: Fri, 3 Apr 2026 13:39:01 +0800 Subject: [PATCH 2/9] fix: correct variadic -1 argument count in error message --- src/parser/ast.rs | 14 +++++++------- src/parser/parse.rs | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/parser/ast.rs b/src/parser/ast.rs index 8b1a296..fc06176 100644 --- a/src/parser/ast.rs +++ b/src/parser/ast.rs @@ -1570,14 +1570,8 @@ fn check_ast_for_call(ex: Call) -> Result { )); } } else { - let expected_args_len_without_default = expected_args_len.saturating_sub(1); - if expected_args_len_without_default > actual_args_len { - return Err(format!( - "expected at least {expected_args_len_without_default} argument(s) in call to '{name}', got {actual_args_len}" - )); - } - if ex.func.variadic > 0 { + let expected_args_len_without_default = expected_args_len.saturating_sub(1); let expected_max_args_len = expected_args_len_without_default + ex.func.variadic as usize; if expected_max_args_len < actual_args_len { @@ -1585,6 +1579,12 @@ fn check_ast_for_call(ex: Call) -> Result { "expected at most {expected_max_args_len} argument(s) in call to '{name}', got {actual_args_len}" )); } + } else if ex.func.variadic == -1 { + if expected_args_len > actual_args_len { + return Err(format!( + "expected at least {expected_args_len} argument(s) in call to '{name}', got {actual_args_len}" + )); + } } } diff --git a/src/parser/parse.rs b/src/parser/parse.rs index ed6dfd6..d852e11 100644 --- a/src/parser/parse.rs +++ b/src/parser/parse.rs @@ -1850,7 +1850,7 @@ mod tests { ("exp()", "expected 1 argument(s) in call to 'exp', got 0"), ( "label_join()", - "expected at least 3 argument(s) in call to 'label_join', got 0", + "expected at least 4 argument(s) in call to 'label_join', got 0", ), ( "sort_by_label()", From 59bed86387b04d326be59573215a236e728ade2d Mon Sep 17 00:00:00 2001 From: Ning Sun Date: Fri, 3 Apr 2026 13:40:14 +0800 Subject: [PATCH 3/9] chore: lint fix --- src/parser/ast.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/parser/ast.rs b/src/parser/ast.rs index fc06176..078c724 100644 --- a/src/parser/ast.rs +++ b/src/parser/ast.rs @@ -1579,12 +1579,10 @@ fn check_ast_for_call(ex: Call) -> Result { "expected at most {expected_max_args_len} argument(s) in call to '{name}', got {actual_args_len}" )); } - } else if ex.func.variadic == -1 { - if expected_args_len > actual_args_len { - return Err(format!( + } else if ex.func.variadic == -1 && expected_args_len > actual_args_len { + return Err(format!( "expected at least {expected_args_len} argument(s) in call to '{name}', got {actual_args_len}" )); - } } } From 5aa62345ca2d4f5a006806aaf2b59b60fb665ba0 Mon Sep 17 00:00:00 2001 From: Ning Sun Date: Fri, 3 Apr 2026 13:41:29 +0800 Subject: [PATCH 4/9] chore: header --- .github/workflows/function-comparison.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/function-comparison.yml b/.github/workflows/function-comparison.yml index 1a30a41..405c563 100644 --- a/.github/workflows/function-comparison.yml +++ b/.github/workflows/function-comparison.yml @@ -1,3 +1,18 @@ +# Copyright 2023 Greptime Team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + name: Function Comparison on: From 36e94df9d5454dfa0487dc69d034aad0113580f6 Mon Sep 17 00:00:00 2001 From: Ning Sun Date: Mon, 11 May 2026 17:57:25 +0800 Subject: [PATCH 5/9] feat: sync functions, remove holt_winters, add start/end/range/step --- src/parser/function.rs | 35 ++++++++++++++++++++++++++++------- src/parser/parse.rs | 14 -------------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/parser/function.rs b/src/parser/function.rs index 78817ba..cf3adc9 100644 --- a/src/parser/function.rs +++ b/src/parser/function.rs @@ -247,6 +247,13 @@ lazy_static! { ValueType::Vector, false ), + function!( + "end", + vec![], + 0, + ValueType::Scalar, + true + ), function!("exp", vec![ValueType::Vector], 0, ValueType::Vector, false), function!( "first_over_time", @@ -337,13 +344,6 @@ lazy_static! { ValueType::Vector, true ), - function!( - "holt_winters", - vec![ValueType::Matrix, ValueType::Scalar, ValueType::Scalar], - 0, - ValueType::Vector, - false - ), function!("hour", vec![ValueType::Vector], 1, ValueType::Vector, false), function!( "idelta", @@ -471,6 +471,13 @@ lazy_static! { false ), function!("pi", vec![], 0, ValueType::Scalar, false), + function!( + "range", + vec![], + 0, + ValueType::Scalar, + true + ), function!( "predict_linear", vec![ValueType::Matrix, ValueType::Scalar], @@ -516,6 +523,20 @@ lazy_static! { false ), function!("sgn", vec![ValueType::Vector], 0, ValueType::Vector, false), + function!( + "start", + vec![], + 0, + ValueType::Scalar, + true + ), + function!( + "step", + vec![], + 0, + ValueType::Scalar, + true + ), function!("sin", vec![ValueType::Vector], 0, ValueType::Vector, false), function!("sinh", vec![ValueType::Vector], 0, ValueType::Vector, false), function!("sort", vec![ValueType::Vector], 0, ValueType::Vector, false), diff --git a/src/parser/parse.rs b/src/parser/parse.rs index d852e11..f62d0e4 100644 --- a/src/parser/parse.rs +++ b/src/parser/parse.rs @@ -1344,20 +1344,6 @@ mod tests { ) }) }), - ("holt_winters(some_metric[5m], 0.5, 0.1)", { - Expr::new_matrix_selector( - Expr::from(VectorSelector::from("some_metric")), - duration::MINUTE_DURATION * 5, - ) - .and_then(|ex| { - Expr::new_call( - get_function("holt_winters").unwrap(), - FunctionArgs::new_args(ex) - .append_args(Expr::from(0.5)) - .append_args(Expr::from(0.1)), - ) - }) - }), // cases from https://prometheus.io/docs/prometheus/latest/querying/functions (r#"absent(nonexistent{job="myjob"})"#, { let name = String::from("nonexistent"); From 3940fe47621d1bf903ba5d54278765e77fb2b5a1 Mon Sep 17 00:00:00 2001 From: Ning Sun Date: Mon, 11 May 2026 22:15:35 +0800 Subject: [PATCH 6/9] chore: format --- src/parser/function.rs | 32 ++++---------------------------- 1 file changed, 4 insertions(+), 28 deletions(-) diff --git a/src/parser/function.rs b/src/parser/function.rs index cf3adc9..b7b2b34 100644 --- a/src/parser/function.rs +++ b/src/parser/function.rs @@ -247,13 +247,7 @@ lazy_static! { ValueType::Vector, false ), - function!( - "end", - vec![], - 0, - ValueType::Scalar, - true - ), + function!("end", vec![], 0, ValueType::Scalar, true), function!("exp", vec![ValueType::Vector], 0, ValueType::Vector, false), function!( "first_over_time", @@ -471,13 +465,7 @@ lazy_static! { false ), function!("pi", vec![], 0, ValueType::Scalar, false), - function!( - "range", - vec![], - 0, - ValueType::Scalar, - true - ), + function!("range", vec![], 0, ValueType::Scalar, true), function!( "predict_linear", vec![ValueType::Matrix, ValueType::Scalar], @@ -523,20 +511,8 @@ lazy_static! { false ), function!("sgn", vec![ValueType::Vector], 0, ValueType::Vector, false), - function!( - "start", - vec![], - 0, - ValueType::Scalar, - true - ), - function!( - "step", - vec![], - 0, - ValueType::Scalar, - true - ), + function!("start", vec![], 0, ValueType::Scalar, true), + function!("step", vec![], 0, ValueType::Scalar, true), function!("sin", vec![ValueType::Vector], 0, ValueType::Vector, false), function!("sinh", vec![ValueType::Vector], 0, ValueType::Vector, false), function!("sort", vec![ValueType::Vector], 0, ValueType::Vector, false), From 1fadd955d1cd26889dff6aed46fab5e0a809b504 Mon Sep 17 00:00:00 2001 From: Ning Sun Date: Tue, 12 May 2026 14:52:55 +0800 Subject: [PATCH 7/9] feat: add vardict test s --- src/parser/ast.rs | 103 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/src/parser/ast.rs b/src/parser/ast.rs index 078c724..2782dac 100644 --- a/src/parser/ast.rs +++ b/src/parser/ast.rs @@ -1574,6 +1574,11 @@ fn check_ast_for_call(ex: Call) -> Result { let expected_args_len_without_default = expected_args_len.saturating_sub(1); let expected_max_args_len = expected_args_len_without_default + ex.func.variadic as usize; + if actual_args_len < expected_args_len_without_default { + return Err(format!( + "expected at least {expected_args_len_without_default} argument(s) in call to '{name}', got {actual_args_len}" + )); + } if expected_max_args_len < actual_args_len { return Err(format!( "expected at most {expected_max_args_len} argument(s) in call to '{name}', got {actual_args_len}" @@ -2740,6 +2745,104 @@ or assert_eq!(expect, stmt.to_string()); } + fn make_call(func_name: &str, arg_count: usize) -> Call { + use crate::parser::function::get_function; + let func = get_function(func_name).unwrap_or_else(|| panic!("unknown function: {func_name}")); + let args: Vec> = (0..arg_count) + .map(|_| Box::new(Expr::VectorSelector(VectorSelector::from("foo")))) + .collect(); + Call { + func, + args: FunctionArgs { args }, + } + } + + #[test] + fn test_call_arity_variadic_zero() { + // floor: arg_types=[Vector], variadic=0 → exact 1 arg required + assert!(check_ast(Expr::Call(make_call("floor", 1))).is_ok()); + + let err = check_ast(Expr::Call(make_call("floor", 0))).unwrap_err(); + assert!( + err.contains("expected 1 argument(s) in call to 'floor', got 0"), + "{err}" + ); + + let err = check_ast(Expr::Call(make_call("floor", 2))).unwrap_err(); + assert!( + err.contains("expected 1 argument(s) in call to 'floor', got 2"), + "{err}" + ); + } + + #[test] + fn test_call_arity_bounded_variadic_single_arg_type() { + // days_in_month: arg_types=[Vector], variadic=1 → min=0, max=1 + // 0 args is valid (default); only "too many" is enforced + assert!(check_ast(Expr::Call(make_call("days_in_month", 1))).is_ok()); + + let err = check_ast(Expr::Call(make_call("days_in_month", 2))).unwrap_err(); + assert!( + err.contains("expected at most 1 argument(s) in call to 'days_in_month', got 2"), + "{err}" + ); + } + + #[test] + fn test_call_arity_bounded_variadic_two_arg_types() { + // round: arg_types=[Vector, Scalar], variadic=1 → min=1, max=2 + let err = check_ast(Expr::Call(make_call("round", 0))).unwrap_err(); + assert!( + err.contains("expected at least 1 argument(s) in call to 'round', got 0"), + "{err}" + ); + + let err = check_ast(Expr::Call(make_call("round", 3))).unwrap_err(); + assert!( + err.contains("expected at most 2 argument(s) in call to 'round', got 3"), + "{err}" + ); + + // info: arg_types=[Vector, Vector], variadic=1 → min=1, max=2 + let err = check_ast(Expr::Call(make_call("info", 0))).unwrap_err(); + assert!( + err.contains("expected at least 1 argument(s) in call to 'info', got 0"), + "{err}" + ); + + let err = check_ast(Expr::Call(make_call("info", 3))).unwrap_err(); + assert!( + err.contains("expected at most 2 argument(s) in call to 'info', got 3"), + "{err}" + ); + } + + #[test] + fn test_call_arity_bounded_variadic_large() { + // histogram_quantiles: arg_types=[Vector, String, Scalar, Scalar], variadic=9 → min=3, max=12 + let err = check_ast(Expr::Call(make_call("histogram_quantiles", 2))).unwrap_err(); + assert!( + err.contains("expected at least 3 argument(s) in call to 'histogram_quantiles', got 2"), + "{err}" + ); + + let err = check_ast(Expr::Call(make_call("histogram_quantiles", 13))).unwrap_err(); + assert!( + err.contains("expected at most 12 argument(s) in call to 'histogram_quantiles', got 13"), + "{err}" + ); + } + + #[test] + fn test_call_arity_unbounded_variadic() { + // label_join: arg_types=[Vector, String, String, String], variadic=-1 → min=4, no max + let err = check_ast(Expr::Call(make_call("label_join", 3))).unwrap_err(); + assert!( + err.contains("expected at least 4 argument(s) in call to 'label_join', got 3"), + "{err}" + ); + } + #[test] fn test_prettify_with_utf8_labels() { // Test that labels with special characters are properly quoted in display From 8935472c645236ce8db3adb760825eec1b5ce7b3 Mon Sep 17 00:00:00 2001 From: Ning Sun Date: Tue, 12 May 2026 15:09:48 +0800 Subject: [PATCH 8/9] refactor: make arg count check easier to read --- src/parser/ast.rs | 97 ++++++++++++++++++++++++++++------------------- 1 file changed, 59 insertions(+), 38 deletions(-) diff --git a/src/parser/ast.rs b/src/parser/ast.rs index 2782dac..d390a92 100644 --- a/src/parser/ast.rs +++ b/src/parser/ast.rs @@ -1559,37 +1559,14 @@ fn check_ast_for_aggregate_expr(ex: AggregateExpr) -> Result { } fn check_ast_for_call(ex: Call) -> Result { - let expected_args_len = ex.func.arg_types.len(); let name = ex.func.name; - let actual_args_len = ex.args.len(); - if ex.func.variadic == 0 { - if expected_args_len != actual_args_len { - return Err(format!( - "expected {expected_args_len} argument(s) in call to '{name}', got {actual_args_len}" - )); - } - } else { - if ex.func.variadic > 0 { - let expected_args_len_without_default = expected_args_len.saturating_sub(1); - let expected_max_args_len = - expected_args_len_without_default + ex.func.variadic as usize; - if actual_args_len < expected_args_len_without_default { - return Err(format!( - "expected at least {expected_args_len_without_default} argument(s) in call to '{name}', got {actual_args_len}" - )); - } - if expected_max_args_len < actual_args_len { - return Err(format!( - "expected at most {expected_max_args_len} argument(s) in call to '{name}', got {actual_args_len}" - )); - } - } else if ex.func.variadic == -1 && expected_args_len > actual_args_len { - return Err(format!( - "expected at least {expected_args_len} argument(s) in call to '{name}', got {actual_args_len}" - )); - } - } + check_call_arity( + ex.func.arg_types.len(), + ex.func.variadic, + ex.args.len(), + name, + )?; // special cases from https://prometheus.io/docs/prometheus/latest/querying/functions if name.eq("exp") { @@ -1606,20 +1583,61 @@ fn check_ast_for_call(ex: Call) -> Result { } } - for (mut idx, actual_arg) in ex.args.args.iter().enumerate() { - // this only happens when function args are variadic - if idx >= ex.func.arg_types.len() { - idx = ex.func.arg_types.len() - 1; + check_args_match_types(&ex.args.args, &ex.func.arg_types, name)?; + Ok(Expr::Call(ex)) +} + +fn check_call_arity( + defined_types: usize, + variadic: i32, + actual: usize, + name: &str, +) -> Result<(), String> { + if variadic == 0 { + if defined_types != actual { + return Err(format!( + "expected {defined_types} argument(s) in call to '{name}', got {actual}" + )); + } + } else if variadic > 0 { + let min_args = defined_types.saturating_sub(1); + let max_args = min_args + variadic as usize; + if actual < min_args { + return Err(format!( + "expected at least {min_args} argument(s) in call to '{name}', got {actual}" + )); } + if actual > max_args { + return Err(format!( + "expected at most {max_args} argument(s) in call to '{name}', got {actual}" + )); + } + } else if variadic == -1 && actual < defined_types { + return Err(format!( + "expected at least {defined_types} argument(s) in call to '{name}', got {actual}" + )); + } + Ok(()) +} +fn check_args_match_types( + args: &[Box], + arg_types: &[ValueType], + name: &str, +) -> Result<(), String> { + for (i, actual_arg) in args.iter().enumerate() { + let expected_idx = if i < arg_types.len() { + i + } else { + arg_types.len() - 1 + }; expect_type( - ex.func.arg_types[idx], + arg_types[expected_idx], Some(actual_arg.value_type()), &format!("call to function '{name}'"), )?; } - - Ok(Expr::Call(ex)) + Ok(()) } fn check_ast_for_unary(ex: UnaryExpr) -> Result { @@ -2747,7 +2765,8 @@ or fn make_call(func_name: &str, arg_count: usize) -> Call { use crate::parser::function::get_function; - let func = get_function(func_name).unwrap_or_else(|| panic!("unknown function: {func_name}")); + let func = + get_function(func_name).unwrap_or_else(|| panic!("unknown function: {func_name}")); let args: Vec> = (0..arg_count) .map(|_| Box::new(Expr::VectorSelector(VectorSelector::from("foo")))) .collect(); @@ -2828,7 +2847,9 @@ or let err = check_ast(Expr::Call(make_call("histogram_quantiles", 13))).unwrap_err(); assert!( - err.contains("expected at most 12 argument(s) in call to 'histogram_quantiles', got 13"), + err.contains( + "expected at most 12 argument(s) in call to 'histogram_quantiles', got 13" + ), "{err}" ); } From 2ceecdb11ec7c29c80d7ffaf3dc65832472c2ad6 Mon Sep 17 00:00:00 2001 From: Ning Sun Date: Wed, 13 May 2026 17:27:58 +0800 Subject: [PATCH 9/9] fix: when variadict is -1 it means the last argument is 0 or more --- src/parser/ast.rs | 49 ++++++++++++++++++++++----------------------- src/parser/parse.rs | 14 +++---------- 2 files changed, 27 insertions(+), 36 deletions(-) diff --git a/src/parser/ast.rs b/src/parser/ast.rs index d390a92..08e4977 100644 --- a/src/parser/ast.rs +++ b/src/parser/ast.rs @@ -1587,35 +1587,27 @@ fn check_ast_for_call(ex: Call) -> Result { Ok(Expr::Call(ex)) } -fn check_call_arity( - defined_types: usize, - variadic: i32, - actual: usize, - name: &str, -) -> Result<(), String> { +fn check_call_arity(nargs: usize, variadic: i32, actual: usize, name: &str) -> Result<(), String> { if variadic == 0 { - if defined_types != actual { - return Err(format!( - "expected {defined_types} argument(s) in call to '{name}', got {actual}" - )); - } - } else if variadic > 0 { - let min_args = defined_types.saturating_sub(1); - let max_args = min_args + variadic as usize; - if actual < min_args { + if nargs != actual { return Err(format!( - "expected at least {min_args} argument(s) in call to '{name}', got {actual}" + "expected {nargs} argument(s) in call to '{name}', got {actual}" )); } - if actual > max_args { + } else { + let na = nargs.saturating_sub(1); + if na > actual { return Err(format!( - "expected at most {max_args} argument(s) in call to '{name}', got {actual}" + "expected at least {na} argument(s) in call to '{name}', got {actual}" )); + } else if variadic > 0 { + let nargsmax = na + variadic as usize; + if nargsmax < actual { + return Err(format!( + "expected at most {nargsmax} argument(s) in call to '{name}', got {actual}" + )); + } } - } else if variadic == -1 && actual < defined_types { - return Err(format!( - "expected at least {defined_types} argument(s) in call to '{name}', got {actual}" - )); } Ok(()) } @@ -2856,10 +2848,17 @@ or #[test] fn test_call_arity_unbounded_variadic() { - // label_join: arg_types=[Vector, String, String, String], variadic=-1 → min=4, no max - let err = check_ast(Expr::Call(make_call("label_join", 3))).unwrap_err(); + // label_join: arg_types=[Vector, String, String, String], variadic=-1 → min=3, no max + let err = check_ast(Expr::Call(make_call("label_join", 2))).unwrap_err(); + assert!( + err.contains("expected at least 3 argument(s) in call to 'label_join', got 2"), + "{err}" + ); + + // sort_by_label: arg_types=[Vector, String], variadic=-1 → min=1, no max + let err = check_ast(Expr::Call(make_call("sort_by_label", 0))).unwrap_err(); assert!( - err.contains("expected at least 4 argument(s) in call to 'label_join', got 3"), + err.contains("expected at least 1 argument(s) in call to 'sort_by_label', got 0"), "{err}" ); } diff --git a/src/parser/parse.rs b/src/parser/parse.rs index f62d0e4..f62c0f4 100644 --- a/src/parser/parse.rs +++ b/src/parser/parse.rs @@ -1836,23 +1836,15 @@ mod tests { ("exp()", "expected 1 argument(s) in call to 'exp', got 0"), ( "label_join()", - "expected at least 4 argument(s) in call to 'label_join', got 0", + "expected at least 3 argument(s) in call to 'label_join', got 0", ), ( "sort_by_label()", - "expected at least 2 argument(s) in call to 'sort_by_label', got 0", + "expected at least 1 argument(s) in call to 'sort_by_label', got 0", ), ( "sort_by_label_desc()", - "expected at least 2 argument(s) in call to 'sort_by_label_desc', got 0", - ), - ( - "sort_by_label(sum(up) by (instance))", - "expected at least 2 argument(s) in call to 'sort_by_label', got 1", - ), - ( - "sort_by_label_desc(sum(up) by (instance))", - "expected at least 2 argument(s) in call to 'sort_by_label_desc', got 1", + "expected at least 1 argument(s) in call to 'sort_by_label_desc', got 0", ), // (r#"label_replace(a, `b`, `c\xff`, `d`, `.*`)"#, ""), ];