From f61586ff376c11924b59f21dff67ed84c955ae22 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Fri, 24 Apr 2026 01:54:41 +0300 Subject: [PATCH] all: fix Allow variadic functions with variable types (fixes #21462) --- vlib/v/ast/ast.v | 6 +- vlib/v/ast/table.v | 16 ++- vlib/v/ast/types.v | 8 +- vlib/v/checker/checker.v | 6 +- vlib/v/checker/fn.v | 98 ++++++++++--------- ...yle_variadic_fn_missing_fixed_args_err.out | 8 ++ ...tyle_variadic_fn_missing_fixed_args_err.vv | 8 ++ ...le_variadic_fn_with_fixed_args.c.must_have | 1 + .../c_style_variadic_fn_with_fixed_args.vv | 9 ++ 9 files changed, 105 insertions(+), 55 deletions(-) create mode 100644 vlib/v/checker/tests/c_style_variadic_fn_missing_fixed_args_err.out create mode 100644 vlib/v/checker/tests/c_style_variadic_fn_missing_fixed_args_err.vv create mode 100644 vlib/v/gen/c/testdata/c_style_variadic_fn_with_fixed_args.c.must_have create mode 100644 vlib/v/gen/c/testdata/c_style_variadic_fn_with_fixed_args.vv diff --git a/vlib/v/ast/ast.v b/vlib/v/ast/ast.v index 91ee6da3f..2c5b7e93d 100644 --- a/vlib/v/ast/ast.v +++ b/vlib/v/ast/ast.v @@ -745,9 +745,9 @@ pub mut: fn (f &Fn) method_equals(o &Fn) bool { return f.params[1..].equals(o.params[1..]) && f.return_type == o.return_type - && f.is_variadic == o.is_variadic && f.language == o.language - && f.generic_names == o.generic_names && f.is_pub == o.is_pub && f.mod == o.mod - && f.name == o.name + && f.is_variadic == o.is_variadic && f.is_c_variadic == o.is_c_variadic + && f.language == o.language && f.generic_names == o.generic_names && f.is_pub == o.is_pub + && f.mod == o.mod && f.name == o.name } @[minify] diff --git a/vlib/v/ast/table.v b/vlib/v/ast/table.v index cd28fbdae..969b51f59 100644 --- a/vlib/v/ast/table.v +++ b/vlib/v/ast/table.v @@ -186,6 +186,12 @@ pub fn (t &Table) fn_type_signature(f &Fn) string { sig += '_' } } + if f.is_c_variadic { + if sig.len > 0 { + sig += '_' + } + sig += 'c_variadic' + } if f.return_type != 0 && f.return_type != void_type { sig += '__${util.no_dots(t.type_to_str(f.return_type)).replace_each(fn_type_escape_seq)}' } @@ -201,7 +207,7 @@ fn (t &Table) fn_type_signature_part(f &Fn, i int, arg Param) string { } sig += 'mut ' } - if i == f.params.len - 1 && f.is_variadic { + if i == f.params.len - 1 && f.is_variadic && !f.is_c_variadic { sig += '...' } sig += t.type_to_str(typ) @@ -224,7 +230,7 @@ pub fn (t &Table) fn_type_source_signature(f &Fn) string { if t.is_fmt && arg.name != '' { sig += '${arg.name} ' } - if i == f.params.len - 1 && f.is_variadic { + if i == f.params.len - 1 && f.is_variadic && !f.is_c_variadic { sig += '...' } sig += t.type_to_str_using_aliases(typ, import_aliases) @@ -232,6 +238,12 @@ pub fn (t &Table) fn_type_source_signature(f &Fn) string { sig += ', ' } } + if f.is_c_variadic { + if f.params.len > 0 { + sig += ', ' + } + sig += '...' + } sig += ')' if f.return_type == ovoid_type { sig += ' ?' diff --git a/vlib/v/ast/types.v b/vlib/v/ast/types.v index 517a9afc7..c3eb64b1d 100644 --- a/vlib/v/ast/types.v +++ b/vlib/v/ast/types.v @@ -1930,13 +1930,19 @@ pub fn (t &Table) fn_signature_using_aliases(func &Fn, import_aliases map[string sb.write_string(' ') } styp := t.type_to_str_using_aliases(typ, import_aliases) - if i == func.params.len - 1 && func.is_variadic { + if i == func.params.len - 1 && func.is_variadic && !func.is_c_variadic { sb.write_string('...') sb.write_string(styp) } else { sb.write_string(styp) } } + if func.is_c_variadic { + if func.params.len > 0 { + sb.write_string(', ') + } + sb.write_string('...') + } sb.write_string(')') if func.return_type != void_type { sb.write_string(' ') diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index 97f1ed3f1..56b568e69 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -6139,7 +6139,8 @@ fn (mut c Checker) infer_fn_value_concrete_type(mut inferred map[string]ast.Type template_fn := (template_final_sym.info as ast.FnType).func actual_fn := (actual_final_sym.info as ast.FnType).func if template_fn.params.len != actual_fn.params.len - || template_fn.is_variadic != actual_fn.is_variadic { + || template_fn.is_variadic != actual_fn.is_variadic + || template_fn.is_c_variadic != actual_fn.is_c_variadic { return false } for i, template_param in template_fn.params { @@ -6170,7 +6171,8 @@ fn (mut c Checker) infer_fn_value_concrete_types(func &ast.Fn, expected_type ast return none } expected_fn := (expected_sym.info as ast.FnType).func - if func.params.len != expected_fn.params.len || func.is_variadic != expected_fn.is_variadic { + if func.params.len != expected_fn.params.len || func.is_variadic != expected_fn.is_variadic + || func.is_c_variadic != expected_fn.is_c_variadic { return none } mut inferred := map[string]ast.Type{} diff --git a/vlib/v/checker/fn.v b/vlib/v/checker/fn.v index a317df23c..f21ee3279 100644 --- a/vlib/v/checker/fn.v +++ b/vlib/v/checker/fn.v @@ -2220,6 +2220,8 @@ fn (mut c Checker) fn_call(mut node ast.CallExpr, mut continue_check &bool) ast. mut has_decompose := false mut has_unresolved_generic_param := false mut nr_multi_values := 0 + variadic_start := variadic_call_arg_start_idx(func, false) + has_typed_variadic := func.is_variadic && !func.is_c_variadic for i, mut call_arg in node.args { if func.params.len == 0 { continue @@ -2231,11 +2233,7 @@ fn (mut c Checker) fn_call(mut node ast.CallExpr, mut continue_check &bool) ast. c.error('cannot have parameter after array decompose', node.pos) } param_i := i + nr_multi_values - mut param := if func.is_variadic && i >= func.params.len - 1 { - func.params.last() - } else { - func.params[param_i] - } + mut param := call_arg_param_for_fn(func, param_i, false) if node.is_fn_var && param.typ.has_flag(.generic) && c.table.cur_fn != unsafe { nil } && c.table.cur_fn.generic_names.len > 0 && c.table.cur_fn.generic_names.len == c.table.cur_concrete_types.len { @@ -2249,7 +2247,7 @@ fn (mut c Checker) fn_call(mut node ast.CallExpr, mut continue_check &bool) ast. // registers if the arg must be passed by ref to disable auto deref args call_arg.should_be_ptr = param.typ.is_ptr() && !param.is_mut if func.is_variadic && call_arg.expr is ast.ArrayDecompose { - if param_i > func.params.len - 1 { + if param_i > variadic_start { c.error('too many arguments in call to `${func.name}`', node.pos) } } @@ -2268,7 +2266,7 @@ fn (mut c Checker) fn_call(mut node ast.CallExpr, mut continue_check &bool) ast. } } already_checked := node.language != .js && call_arg.expr is ast.CallExpr - if func.is_variadic && param_i >= func.params.len - 1 { + if has_typed_variadic && param_i >= variadic_start { param_sym := c.table.sym(param.typ) mut expected_type := param.typ if param_sym.info is ast.Array { @@ -2370,7 +2368,7 @@ fn (mut c Checker) fn_call(mut node ast.CallExpr, mut continue_check &bool) ast. || has_unresolved_generic_param } param_typ_sym := c.table.sym(param.typ) - if func.is_variadic && arg_typ.has_flag(.variadic) && args_len - 1 > i { + if has_typed_variadic && arg_typ.has_flag(.variadic) && args_len - 1 > i { c.error('when forwarding a variadic variable, it must be the final argument', call_arg.pos) } @@ -2432,7 +2430,7 @@ fn (mut c Checker) fn_call(mut node ast.CallExpr, mut continue_check &bool) ast. arg_typ_sym = c.table.sym(arg_typ) mut final_param_sym := unsafe { param_typ_sym } mut final_param_typ := param.typ - if func.is_variadic && param_typ_sym.info is ast.Array { + if has_typed_variadic && param_typ_sym.info is ast.Array { final_param_typ = param_typ_sym.info.elem_type final_param_sym = c.table.sym(final_param_typ) } @@ -2449,7 +2447,7 @@ fn (mut c Checker) fn_call(mut node ast.CallExpr, mut continue_check &bool) ast. || param.typ == ast.nil_type || final_param_sym.idx == ast.nil_type_idx) && !call_arg.typ.is_any_kind_of_pointer() && func.language == .v && !call_arg.expr.is_lvalue() && !c.pref.translated && !c.file.is_translated - && !(func.is_variadic && final_param_sym.idx == ast.voidptr_type_idx) + && !(has_typed_variadic && final_param_sym.idx == ast.voidptr_type_idx) && !func.is_c_variadic && func.name !in ['json.encode', 'json.encode_pretty'] { c.error('expression cannot be passed as `voidptr`', call_arg.expr.pos()) } @@ -2537,7 +2535,7 @@ fn (mut c Checker) fn_call(mut node ast.CallExpr, mut continue_check &bool) ast. } } else if arg_typ_sym.info is ast.MultiReturn { arg_typs := arg_typ_sym.info.types - if !(func.is_variadic && i >= func.params.len - 1) { + if !(has_typed_variadic && param_i >= variadic_start) { if arg_typ_sym.info.types.len > func.params.len { c.error('trying to pass ${arg_typ_sym.info.types.len} argument(s), but function expects ${func.params.len} argument(s)', node.pos) @@ -2557,11 +2555,7 @@ fn (mut c Checker) fn_call(mut node ast.CallExpr, mut continue_check &bool) ast. } out: for n in 0 .. arg_typ_sym.info.types.len { curr_arg := arg_typs[n] - multi_param := if func.is_variadic && i >= func.params.len - 1 { - func.params.last() - } else { - func.params[n + param_i] - } + multi_param := call_arg_param_for_fn(func, n + param_i, false) c.check_expected_call_arg(curr_arg, c.unwrap_generic(multi_param.typ), node.language, call_arg) or { c.error('${err.msg()} in argument ${param_i + n + 1} to `${fn_name}` from ${c.table.type_to_str(arg_typ)}', @@ -2714,11 +2708,7 @@ fn (mut c Checker) fn_call(mut node ast.CallExpr, mut continue_check &bool) ast. } if func.generic_names.len > 0 { for i, mut call_arg in node.args { - param := if func.is_variadic && i >= func.params.len - 1 { - func.params.last() - } else { - func.params[i] - } + param := call_arg_param_for_fn(func, i, false) if param.typ.has_flag(.generic) { if unwrap_typ := c.table.convert_generic_param_type(param, func.generic_names, concrete_types) @@ -3305,11 +3295,7 @@ fn (mut c Checker) method_call(mut node ast.CallExpr, mut continue_check &bool) targ := c.check_expr_option_or_result_call(arg.expr, c.expr(mut arg.expr)) arg.typ = targ - param := if info.func.is_variadic && i >= info.func.params.len - 1 { - info.func.params.last() - } else { - info.func.params[i] - } + param := call_arg_param_for_fn(info.func, i, false) // registers if the arg must be passed by ref to disable auto deref args arg.should_be_ptr = param.typ.is_ptr() && !param.is_mut if c.table.sym(param.typ).kind == .interface { @@ -3570,11 +3556,7 @@ fn (mut c Checker) method_call(mut node ast.CallExpr, mut continue_check &bool) for i, mut arg in node.args { if i > 0 || exp_arg_typ == ast.no_type { - exp_arg_typ = if method.is_variadic && i >= method.params.len - 1 { - method.params.last().typ - } else { - method.params[i + 1].typ - } + exp_arg_typ = call_arg_param_for_fn(method, i, true).typ if !c.inside_recheck { arg.ct_expr = c.comptime.is_comptime(arg.expr) } @@ -3593,11 +3575,7 @@ fn (mut c Checker) method_call(mut node ast.CallExpr, mut continue_check &bool) parent_sym := c.table.sym(parent_type) if parent_sym.info is ast.Struct && parent_sym.info.is_generic { if f := parent_sym.find_method(method_name) { - exp_arg_typ = if f.is_variadic && i >= f.params.len - 1 { - f.params.last().typ - } else { - f.params[i + 1].typ - } + exp_arg_typ = call_arg_param_for_fn(f, i, true).typ } } } @@ -3638,27 +3616,25 @@ fn (mut c Checker) method_call(mut node ast.CallExpr, mut continue_check &bool) arg.pos) } } - if method.is_variadic && got_arg_typ.has_flag(.variadic) && node.args.len - 1 > i { + variadic_start := variadic_call_arg_start_idx(method, true) + has_typed_variadic := method.is_variadic && !method.is_c_variadic + if has_typed_variadic && got_arg_typ.has_flag(.variadic) && node.args.len - 1 > i { c.error('when forwarding a variadic variable, it must be the final argument', arg.pos) } mut final_arg_sym := unsafe { exp_arg_sym } mut final_arg_typ := exp_arg_typ - if method.is_variadic && exp_arg_sym.info is ast.Array { + if has_typed_variadic && exp_arg_sym.info is ast.Array { final_arg_typ = exp_arg_sym.info.elem_type final_arg_sym = c.table.sym(final_arg_typ) } - param := if method.is_variadic && i >= method.params.len - 1 { - method.params.last() - } else { - method.params[i + 1] - } + param := call_arg_param_for_fn(method, i, true) if method.is_variadic && arg.expr is ast.ArrayDecompose { - if i > method.params.len - 2 { + if i > variadic_start { c.error('too many arguments in call to `${method.name}`', node.pos) } } - if method.is_variadic && i >= method.params.len - 2 { + if has_typed_variadic && i >= variadic_start { param_sym := c.table.sym(param.typ) mut expected_type := param.typ if param_sym.kind == .array { @@ -4210,6 +4186,32 @@ fn min_required_call_params(f &ast.Fn, is_method_call bool) int { return required } +@[inline] +fn variadic_call_arg_start_idx(f &ast.Fn, is_method_call bool) int { + start_idx := if is_method_call && f.params.len > 0 { 1 } else { 0 } + mut fixed_args := f.params.len - start_idx + if f.is_variadic && !f.is_c_variadic && fixed_args > 0 { + fixed_args-- + } + return if fixed_args >= 0 { fixed_args } else { 0 } +} + +@[inline] +fn call_arg_param_for_fn(f &ast.Fn, arg_idx int, is_method_call bool) ast.Param { + start_idx := if is_method_call && f.params.len > 0 { 1 } else { 0 } + variadic_start := variadic_call_arg_start_idx(f, is_method_call) + if f.is_variadic && !f.is_c_variadic && arg_idx >= variadic_start { + return f.params.last() + } + param_idx := start_idx + arg_idx + if param_idx < f.params.len { + return f.params[param_idx] + } + return ast.Param{ + typ: ast.any_type + } +} + fn call_can_fill_optional_args(node &ast.CallExpr) bool { if node.args.any(it.expr is ast.ArrayDecompose) { return false @@ -4242,8 +4244,10 @@ fn (mut c Checker) check_expected_arg_count(mut node ast.CallExpr, f &ast.Fn) ! if f.is_variadic { node.is_variadic = f.is_variadic node.is_c_variadic = f.is_c_variadic - min_required_params-- - c.markused_array_method(!c.is_builtin_mod, '') + if !f.is_c_variadic { + min_required_params-- + c.markused_array_method(!c.is_builtin_mod, '') + } } else { has_decompose := node.args.any(it.expr is ast.ArrayDecompose) if has_decompose { diff --git a/vlib/v/checker/tests/c_style_variadic_fn_missing_fixed_args_err.out b/vlib/v/checker/tests/c_style_variadic_fn_missing_fixed_args_err.out new file mode 100644 index 000000000..8b705e49e --- /dev/null +++ b/vlib/v/checker/tests/c_style_variadic_fn_missing_fixed_args_err.out @@ -0,0 +1,8 @@ +vlib/v/checker/tests/c_style_variadic_fn_missing_fixed_args_err.vv:7:2: error: expected 2 arguments, but got 1 + 5 | + 6 | fn main() { + 7 | hello(1) + | ~~~~~~~~ + 8 | } +Details: have (int literal) + want (int, f64) diff --git a/vlib/v/checker/tests/c_style_variadic_fn_missing_fixed_args_err.vv b/vlib/v/checker/tests/c_style_variadic_fn_missing_fixed_args_err.vv new file mode 100644 index 000000000..cf4d9e573 --- /dev/null +++ b/vlib/v/checker/tests/c_style_variadic_fn_missing_fixed_args_err.vv @@ -0,0 +1,8 @@ +fn hello(i int, f f64, ...) { + _ = i + _ = f +} + +fn main() { + hello(1) +} diff --git a/vlib/v/gen/c/testdata/c_style_variadic_fn_with_fixed_args.c.must_have b/vlib/v/gen/c/testdata/c_style_variadic_fn_with_fixed_args.c.must_have new file mode 100644 index 000000000..21910bc27 --- /dev/null +++ b/vlib/v/gen/c/testdata/c_style_variadic_fn_with_fixed_args.c.must_have @@ -0,0 +1 @@ +VV_LOC void main__hello(int i, f64 f, ... ); diff --git a/vlib/v/gen/c/testdata/c_style_variadic_fn_with_fixed_args.vv b/vlib/v/gen/c/testdata/c_style_variadic_fn_with_fixed_args.vv new file mode 100644 index 000000000..bb151d834 --- /dev/null +++ b/vlib/v/gen/c/testdata/c_style_variadic_fn_with_fixed_args.vv @@ -0,0 +1,9 @@ +fn hello(i int, f f64, ...) { + assert i == 1 + assert f == 2.0 +} + +fn main() { + hello(1, 2.0) + hello(1, 2.0, 'x', 3) +} -- 2.39.5