From 2c805927b7d017918b50933c46c95c1fbdc2f7a5 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 11 Mar 2026 12:38:36 +0300 Subject: [PATCH] parser: add support for optional parameters in function declarations (fixes #24910) --- doc/docs.md | 15 ++++ vlib/v/checker/fn.v | 69 +++++++++++++++++-- vlib/v/parser/fn.v | 20 +++++- .../options/option_optional_param_test.v | 35 ++++++++++ 4 files changed, 131 insertions(+), 8 deletions(-) create mode 100644 vlib/v/tests/options/option_optional_param_test.v diff --git a/doc/docs.md b/doc/docs.md index c3ce6308a..3ce2e8f46 100644 --- a/doc/docs.md +++ b/doc/docs.md @@ -4317,6 +4317,21 @@ fn main() { } ``` +Trailing option-typed parameters can also be omitted in function calls. +When they are not passed, V supplies `none`: + +```v +fn connect(url string, timeout ?int) { + actual_timeout := timeout or { 1000 } + println('${url} -> ${actual_timeout}') +} + +fn main() { + connect('https://vlang.io') + connect('https://vlang.io', 5000) +} +``` + V used to combine `Option` and `Result` into one type, now they are separate. The amount of work required to "upgrade" a function to an option/result function is minimal; diff --git a/vlib/v/checker/fn.v b/vlib/v/checker/fn.v index 50d56e7a0..c7c7bf7eb 100644 --- a/vlib/v/checker/fn.v +++ b/vlib/v/checker/fn.v @@ -1071,7 +1071,7 @@ fn (mut c Checker) fn_call(mut node ast.CallExpr, mut continue_check &bool) ast. full_fkey := if fn_name_has_dot { fkey } else { c.mod + '.' + fkey } c.generic_call_positions[c.build_generic_call_key(full_fkey, concrete_types)] = node.pos } - args_len := node.args.len + mut args_len := node.args.len if node.kind == .jsawait { if node.args.len > 1 { c.error('JS.await expects 1 argument, a promise value (e.g `JS.await(fs.read())`', @@ -1590,6 +1590,7 @@ fn (mut c Checker) fn_call(mut node ast.CallExpr, mut continue_check &bool) ast. node.return_type = func.return_type return func.return_type } + args_len = node.args.len } // println / eprintln / panic can print anything if args_len > 0 && fn_name in print_everything_fns { @@ -3146,13 +3147,53 @@ fn (mut c Checker) post_process_generic_fns() ! { } } +fn min_required_call_params(f &ast.Fn, is_method_call bool) int { + start_idx := if is_method_call && f.params.len > 0 { 1 } else { 0 } + if start_idx >= f.params.len { + return 0 + } + mut required := f.params.len - start_idx + mut idx := f.params.len - 1 + for idx >= start_idx { + if f.params[idx].typ.has_flag(.option) { + required-- + idx-- + continue + } + break + } + return required +} + +fn call_can_fill_optional_args(node ast.CallExpr) bool { + if node.args.any(it.expr is ast.ArrayDecompose) { + return false + } + return !node.args.any(it.expr is ast.CallExpr && it.expr.nr_ret_values > 1) +} + +fn fill_trailing_optional_call_args(mut node ast.CallExpr, f &ast.Fn) { + start_idx := if node.is_method && f.params.len > 0 { 1 } else { 0 } + expected_args := f.params.len - start_idx + for i := node.args.len; i < expected_args; i++ { + if !f.params[start_idx + i].typ.has_flag(.option) { + break + } + node.args << ast.CallArg{ + expr: ast.None{ + pos: node.pos + } + pos: node.pos + } + } +} + fn (mut c Checker) check_expected_arg_count(mut node ast.CallExpr, f &ast.Fn) ! { mut nr_args := node.args.len nr_params := if node.is_method && f.params.len > 0 { f.params.len - 1 } else { f.params.len } - mut min_required_params := f.params.len - if node.is_method { - min_required_params-- - } + optional_required_params := min_required_call_params(f, node.is_method) + mut min_required_params := optional_required_params + can_fill_optional_args := call_can_fill_optional_args(node) if f.is_variadic { node.is_variadic = f.is_variadic node.is_c_variadic = f.is_c_variadic @@ -3205,8 +3246,10 @@ fn (mut c Checker) check_expected_arg_count(mut node ast.CallExpr, f &ast.Fn) ! } if nr_args < min_required_params { if min_required_params == nr_args + 1 { + start_idx := if node.is_method && f.params.len > 0 { 1 } else { 0 } + last_required_param := f.params[start_idx + min_required_params - 1] // params struct? - last_typ := f.params.last().typ + last_typ := last_required_param.typ last_sym := c.table.sym(last_typ) if last_sym.info is ast.Struct { is_params := last_sym.info.attrs.any(it.name == 'params' && !it.has_arg) @@ -3229,7 +3272,7 @@ fn (mut c Checker) check_expected_arg_count(mut node ast.CallExpr, f &ast.Fn) ! first_sym := c.table.sym(first_typ) */ if last_sym.info is ast.Struct { - if last_sym.name == 'main.Context' && f.params.last().name == 'ctx' { // TODO use int comparison for perf + if last_sym.name == 'main.Context' && last_required_param.name == 'ctx' { // TODO use int comparison for perf // c.error('got ctx ${first_sym.name}', node.pos) return } @@ -3254,6 +3297,18 @@ fn (mut c Checker) check_expected_arg_count(mut node ast.CallExpr, f &ast.Fn) ! pos: unexpected_args_pos ) return error('') + } else if !f.is_variadic && nr_args < nr_params && optional_required_params < nr_params { + if !can_fill_optional_args { + c.fn_call_error_have_want( + nr_params: nr_params + nr_args: nr_args + params: f.params + args: node.args + pos: node.pos + ) + return error('') + } + fill_trailing_optional_call_args(mut node, f) } } diff --git a/vlib/v/parser/fn.v b/vlib/v/parser/fn.v index 42e10a6f4..92cc2baae 100644 --- a/vlib/v/parser/fn.v +++ b/vlib/v/parser/fn.v @@ -54,7 +54,8 @@ fn (mut p Parser) call_expr(language ast.Language, mod string) ast.CallExpr { args := p.call_args() if p.tok.kind != .rpar && !p.pref.is_vls { params := p.table.fns[fn_name] or { unsafe { p.table.fns['${p.mod}.${fn_name}'] } }.params - if args.len < params.len && p.prev_tok.kind != .comma { + min_required_params := min_required_call_args(params) + if args.len < min_required_params && p.prev_tok.kind != .comma { pos := if p.tok.kind == .eof { p.prev_tok.pos() } else { p.tok.pos() } p.unexpected_with_pos(pos, expecting: '`,`') } else if args.len > params.len { @@ -125,6 +126,23 @@ fn (mut p Parser) call_expr(language ast.Language, mod string) ast.CallExpr { } } +fn min_required_call_args(params []ast.Param) int { + if params.len == 0 { + return 0 + } + mut required := params.len + mut idx := params.len - 1 + for idx >= 0 { + if params[idx].typ.has_flag(.option) { + required-- + idx-- + continue + } + break + } + return required +} + fn (mut p Parser) call_kind(fn_name string) ast.CallKind { if fn_name.len < 3 || fn_name.len > 20 { return .unknown diff --git a/vlib/v/tests/options/option_optional_param_test.v b/vlib/v/tests/options/option_optional_param_test.v new file mode 100644 index 000000000..ada082af6 --- /dev/null +++ b/vlib/v/tests/options/option_optional_param_test.v @@ -0,0 +1,35 @@ +fn with_optional_delta(base int, delta ?int) int { + actual_delta := delta or { 0 } + return base + actual_delta +} + +fn optional_only(value ?int) int { + return value or { -1 } +} + +struct OptionalGreeter {} + +fn (_ OptionalGreeter) add(base int, delta ?int) int { + return with_optional_delta(base, delta) +} + +type OptionalGreeterFn = fn (int, ?int) int + +fn test_trailing_optional_params_can_be_omitted_in_free_function_calls() { + assert with_optional_delta(5) == 5 + assert with_optional_delta(5, 2) == 7 + assert optional_only() == -1 + assert optional_only(8) == 8 +} + +fn test_trailing_optional_params_can_be_omitted_in_method_calls() { + greeter := OptionalGreeter{} + assert greeter.add(5) == 5 + assert greeter.add(5, 2) == 7 +} + +fn test_trailing_optional_params_can_be_omitted_in_fn_variable_calls() { + handler := OptionalGreeterFn(with_optional_delta) + assert handler(5) == 5 + assert handler(5, 2) == 7 +} -- 2.39.5