From e2dd33a05b1f230e3daa142885c05c59d445ca9b Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Tue, 14 Apr 2026 12:45:28 +0300 Subject: [PATCH] all: Allow disabling explicit mutability (fixes #24721) --- vlib/v/checker/checker.v | 17 +++---- vlib/v/checker/fn.v | 26 +++++++++- vlib/v/checker/if.v | 2 +- ...bility.disable_explicit_mutability.run.out | 1 + .../tests/disable_explicit_mutability.vv | 35 ++++++++++++++ vlib/v/compiler_errors_test.v | 5 +- vlib/v/help/build/build.txt | 4 ++ vlib/v/parser/assign.v | 2 +- vlib/v/parser/expr.v | 2 +- vlib/v/parser/fn.v | 13 +++-- vlib/v/parser/if_match.v | 2 +- vlib/v/parser/parser.v | 11 +++++ vlib/v/pref/pref.v | 47 ++++++++++--------- vlib/v/pref/pref_test.v | 13 +++++ 14 files changed, 141 insertions(+), 39 deletions(-) create mode 100644 vlib/v/checker/tests/disable_explicit_mutability.disable_explicit_mutability.run.out create mode 100644 vlib/v/checker/tests/disable_explicit_mutability.vv diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index 5d61ebf59..0b9ad2647 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -3006,7 +3006,7 @@ fn (mut c Checker) selector_expr(mut node ast.SelectorExpr) ast.Type { return node.typ } if final_sym.kind == .struct { - if c.smartcast_mut_pos != token.Pos{} { + if c.smartcast_mut_pos != token.Pos{} && !c.implicit_mutability_enabled() { c.note('smartcasting requires either an immutable value, or an explicit mut keyword before the value', c.smartcast_mut_pos) } @@ -3015,7 +3015,7 @@ fn (mut c Checker) selector_expr(mut node ast.SelectorExpr) ast.Type { c.error(suggestion.say(unknown_field_msg), node.pos) return ast.void_type } - if c.smartcast_mut_pos != token.Pos{} { + if c.smartcast_mut_pos != token.Pos{} && !c.implicit_mutability_enabled() { c.note('smartcasting requires either an immutable value, or an explicit mut keyword before the value', c.smartcast_mut_pos) } @@ -6564,9 +6564,9 @@ fn (mut c Checker) smartcast(mut expr ast.Expr, cur_type ast.Type, to_type_ ast. if field != unsafe { nil } { smartcasts << field.smartcasts } - // smartcast either if the value is immutable or if the mut argument is explicitly given - if !is_mut || expr.is_mut || is_option_unwrap || orig_type.has_flag(.option) - || allow_mut_selector_smartcast { + // smartcast either if the value is immutable or if mutability is allowed here + if !is_mut || expr.is_mut || c.implicit_mutability_enabled() || is_option_unwrap + || orig_type.has_flag(.option) || allow_mut_selector_smartcast { sc_type := if scope_smartcast_type != ast.no_type { scope_smartcast_type } else { @@ -6616,8 +6616,9 @@ fn (mut c Checker) smartcast(mut expr ast.Expr, cur_type ast.Type, to_type_ ast. .no_comptime } } - // smartcast either if the value is immutable or if the mut argument is explicitly given - if (!is_mut || expr.is_mut || is_option_unwrap) && !is_already_casted { + // smartcast either if the value is immutable or if mutability is allowed here + if (!is_mut || expr.is_mut || c.implicit_mutability_enabled() + || is_option_unwrap) && !is_already_casted { sc_type := if scope_smartcast_type != ast.no_type { scope_smartcast_type } else { @@ -6672,7 +6673,7 @@ fn (mut c Checker) smartcast(mut expr ast.Expr, cur_type ast.Type, to_type_ ast. } else { scope.register(new_var) } - } else if is_mut && !expr.is_mut { + } else if is_mut && !expr.is_mut && !c.implicit_mutability_enabled() { c.smartcast_mut_pos = expr.pos } } diff --git a/vlib/v/checker/fn.v b/vlib/v/checker/fn.v index 7eba0985b..bb40d5c8d 100644 --- a/vlib/v/checker/fn.v +++ b/vlib/v/checker/fn.v @@ -8,6 +8,24 @@ import os const print_everything_fns = ['println', 'print', 'eprintln', 'eprint', 'panic'] +@[inline] +fn (c &Checker) implicit_mutability_enabled() bool { + return c.pref.disable_explicit_mutability + && (!os.dir(c.file.path).contains('vlib') || c.file.path.ends_with('.vv')) +} + +@[inline] +fn (c &Checker) implicit_mut_call_arg(param ast.Param, arg ast.CallArg) ast.CallArg { + if !c.implicit_mutability_enabled() || arg.is_mut || !param.is_mut + || param.typ.share() != .mut_t { + return arg + } + return ast.CallArg{ + ...arg + is_mut: true + } +} + fn (mut c Checker) check_os_raw_io_call(node &ast.CallExpr, func &ast.Fn, concrete_types []ast.Type, arg_offset int) { if func.mod != 'os' || !func.is_method { return @@ -2056,6 +2074,8 @@ fn (mut c Checker) fn_call(mut node ast.CallExpr, mut continue_check &bool) ast. c.error('function with `shared` arguments cannot be called inside `lock`/`rlock` block', call_arg.pos) } + call_arg = c.implicit_mut_call_arg(param, call_arg) + node.args[i] = call_arg if call_arg.is_mut { to_lock, pos := c.fail_if_immutable(mut call_arg.expr) if !call_arg.expr.is_lvalue() { @@ -2927,6 +2947,8 @@ fn (mut c Checker) method_call(mut node ast.CallExpr, mut continue_check &bool) c.error('method with `shared` arguments cannot be called inside `lock`/`rlock` block', arg.pos) } + arg = c.implicit_mut_call_arg(param, arg) + node.args[i] = arg if arg.is_mut { to_lock, pos := c.fail_if_immutable(mut arg.expr) if !param.is_mut { @@ -2973,7 +2995,7 @@ fn (mut c Checker) method_call(mut node ast.CallExpr, mut continue_check &bool) } } if left_sym.kind in [.struct, .aggregate, .interface, .sum_type] { - if c.smartcast_mut_pos != token.Pos{} { + if c.smartcast_mut_pos != token.Pos{} && !c.implicit_mutability_enabled() { c.note('smartcasting requires either an immutable value, or an explicit mut keyword before the value', c.smartcast_mut_pos) } @@ -3278,6 +3300,8 @@ fn (mut c Checker) method_call(mut node ast.CallExpr, mut continue_check &bool) c.error('method with `shared` arguments cannot be called inside `lock`/`rlock` block', arg.pos) } + arg = c.implicit_mut_call_arg(param, arg) + node.args[i] = arg if arg.is_mut { to_lock, pos := c.fail_if_immutable(mut arg.expr) if !param_is_mut { diff --git a/vlib/v/checker/if.v b/vlib/v/checker/if.v index c5df39734..e0e377aec 100644 --- a/vlib/v/checker/if.v +++ b/vlib/v/checker/if.v @@ -747,7 +747,7 @@ fn (mut c Checker) smartcast_if_conds(mut node ast.Expr, mut scope ast.Scope, co if mut node.left is ast.Ident && (left_sym.kind == .interface && right_sym.kind != .interface) { v := scope.find_var(node.left.name) or { &ast.Var{} } - if v.is_mut && !node.left.is_mut { + if v.is_mut && !node.left.is_mut && !c.implicit_mutability_enabled() { c.error('smart casting a mutable interface value requires `if mut ${node.left.name} is ...`', node.left.pos) } diff --git a/vlib/v/checker/tests/disable_explicit_mutability.disable_explicit_mutability.run.out b/vlib/v/checker/tests/disable_explicit_mutability.disable_explicit_mutability.run.out new file mode 100644 index 000000000..17cc5a829 --- /dev/null +++ b/vlib/v/checker/tests/disable_explicit_mutability.disable_explicit_mutability.run.out @@ -0,0 +1 @@ +1 2 2 2 Int(2) diff --git a/vlib/v/checker/tests/disable_explicit_mutability.vv b/vlib/v/checker/tests/disable_explicit_mutability.vv new file mode 100644 index 000000000..f5b6e0f47 --- /dev/null +++ b/vlib/v/checker/tests/disable_explicit_mutability.vv @@ -0,0 +1,35 @@ +type Int = int | u8 + +struct Counter { +mut: + value int +} + +fn bump(mut values []int) { + values << 2 +} + +fn inc_copy(value int) int { + value += 1 + return value +} + +fn main() { + counter := Counter{} + counter.value++ + arr := [1] + arr << 2 + nums := [1] + bump(nums) + i := Int(0) + if i is int { + i = 1 + } + match i { + int { + i = 2 + } + u8 {} + } + println('${counter.value} ${arr.len} ${nums.len} ${inc_copy(1)} ${i}') +} diff --git a/vlib/v/compiler_errors_test.v b/vlib/v/compiler_errors_test.v index 71d631656..21da5e6ae 100644 --- a/vlib/v/compiler_errors_test.v +++ b/vlib/v/compiler_errors_test.v @@ -97,9 +97,10 @@ fn test_all() { su_dir := 'vlib/v/tests/skip_unused' no_closures_dir := 'vlib/v/tests/no_closures' js_checker_tests := ['js_number_requires_explicit_cast.vv'] + disable_explicit_mutability_tests := ['disable_explicit_mutability.vv'] checker_tests := get_tests_in_dir(checker_dir, false).filter(!it.contains('with_check_option') - && it !in js_checker_tests) + && it !in js_checker_tests && it !in disable_explicit_mutability_tests) parser_tests := get_tests_in_dir(parser_dir, false) scanner_tests := get_tests_in_dir(scanner_dir, false) global_tests := get_tests_in_dir(global_dir, false) @@ -180,6 +181,8 @@ fn test_all() { tasks.add('', global_dir, '-enable-globals', '.out', global_tests, false) tasks.add('', module_dir, '-prod run', '.out', module_tests, true) tasks.add('', run_dir, 'run', '.run.out', run_tests, false) + tasks.add('', checker_dir, '-disable-explicit-mutability run', '.disable_explicit_mutability.run.out', + disable_explicit_mutability_tests, false) tasks.add('', checker_with_check_option_dir, '-check', '.out', checker_with_check_option_tests, false) tasks.add('', no_closures_dir, '-no-closures run', '.out', no_closures_tests, false) diff --git a/vlib/v/help/build/build.txt b/vlib/v/help/build/build.txt index 7cc65470f..fcecc89a8 100644 --- a/vlib/v/help/build/build.txt +++ b/vlib/v/help/build/build.txt @@ -139,6 +139,10 @@ NB: the build flags are shared with the run command too: extensively (like `math.big`), without constantly edititing their source code to remove/add the tag. Note: this option overrides -no-bounds-checking . + -disable-explicit-mutability, --disable-explicit-mutability + Allow ordinary variables and mutable call arguments in user code to omit explicit `mut`. + `mut`/`shared`/`atomic` in function and method signatures remain unchanged. + -prof, -profile Compile the executable with all functions profiled. The profile results will be stored in `file.txt`. diff --git a/vlib/v/parser/assign.v b/vlib/v/parser/assign.v index f323a3d4b..da973db73 100644 --- a/vlib/v/parser/assign.v +++ b/vlib/v/parser/assign.v @@ -232,7 +232,7 @@ fn (mut p Parser) partial_assign_stmt(left []ast.Expr) ast.Stmt { name: lx.name expr: if left.len == right.len { right[i] } else { ast.empty_expr } share: share - is_mut: lx.is_mut || p.inside_for + is_mut: p.scope_var_is_mut(lx.is_mut || p.inside_for) is_static: is_static is_volatile: is_volatile pos: lx.pos diff --git a/vlib/v/parser/expr.v b/vlib/v/parser/expr.v index ba9af6967..8146a8e3e 100644 --- a/vlib/v/parser/expr.v +++ b/vlib/v/parser/expr.v @@ -1141,7 +1141,7 @@ fn (mut p Parser) lambda_expr() ?ast.LambdaExpr { p.scope.register(ast.Var{ name: ident.name - is_mut: ident.is_mut + is_mut: p.scope_var_is_mut(ident.is_mut) is_stack_obj: true pos: ident.pos is_used: true diff --git a/vlib/v/parser/fn.v b/vlib/v/parser/fn.v index bf810805b..f86b82fd8 100644 --- a/vlib/v/parser/fn.v +++ b/vlib/v/parser/fn.v @@ -871,12 +871,17 @@ run them via `v file.v` instead', scope: unsafe { nil } } } + effective_is_mut := if is_method && k == 0 { + param.is_mut + } else { + p.scope_var_is_mut(param.is_mut) + } is_stack_obj := !param.typ.has_flag(.shared_f) && (param.is_mut || param.typ.is_ptr()) p.scope.register(ast.Var{ name: param.name typ: param.typ generic_typ: if param.typ.has_flag(.generic) { param.typ } else { ast.Type(0) } - is_mut: param.is_mut + is_mut: effective_is_mut is_auto_deref: param.is_mut is_stack_obj: is_stack_obj pos: param.pos @@ -1238,7 +1243,7 @@ fn (mut p Parser) anon_fn() ast.AnonFn { name: param.name typ: param.typ generic_typ: if param.typ.has_flag(.generic) { param.typ } else { ast.Type(0) } - is_mut: param.is_mut + is_mut: p.scope_var_is_mut(param.is_mut) is_auto_deref: param.is_mut pos: param.pos is_used: true @@ -1690,12 +1695,12 @@ fn (mut p Parser) closure_vars() []ast.Param { has_inherited: var.is_inherited is_used: false is_changed: false - is_mut: is_mut + is_mut: p.scope_var_is_mut(is_mut) }) vars << ast.Param{ pos: var_pos name: var_name - is_mut: is_mut + is_mut: p.scope_var_is_mut(is_mut) is_atomic: is_atomic is_shared: is_shared } diff --git a/vlib/v/parser/if_match.v b/vlib/v/parser/if_match.v index 845748fee..82ba84a66 100644 --- a/vlib/v/parser/if_match.v +++ b/vlib/v/parser/if_match.v @@ -112,7 +112,7 @@ fn (mut p Parser) if_expr(is_comptime bool, is_expr bool) ast.IfExpr { is_mut = true p.next() } - var.is_mut = is_mut + var.is_mut = p.scope_var_is_mut(is_mut) var.pos = p.tok.pos() var.name = p.check_name() var_names << var.name diff --git a/vlib/v/parser/parser.v b/vlib/v/parser/parser.v index 46f5f09f6..5550c2197 100644 --- a/vlib/v/parser/parser.v +++ b/vlib/v/parser/parser.v @@ -2232,6 +2232,17 @@ fn (mut p Parser) index_expr(left ast.Expr, is_gated bool) ast.IndexExpr { } } +@[inline] +fn (p &Parser) implicit_mutability_enabled() bool { + return p.pref.disable_explicit_mutability + && (!p.inside_vlib_file || p.file_path.ends_with('.vv')) +} + +@[inline] +fn (p &Parser) scope_var_is_mut(is_mut bool) bool { + return is_mut || p.implicit_mutability_enabled() +} + fn (mut p Parser) dot_expr(left ast.Expr) ast.Expr { prev_line := p.prev_tok.pos().line_nr p.next() diff --git a/vlib/v/pref/pref.v b/vlib/v/pref/pref.v index caf78e386..8e23ff278 100644 --- a/vlib/v/pref/pref.v +++ b/vlib/v/pref/pref.v @@ -187,27 +187,28 @@ pub mut: trace_fns []string // when set, tracing will be done only for functions, whose names match the listed patterns. compress bool // when set, use `upx` to compress the generated executable // generating_vh bool - no_builtin bool // Skip adding the `builtin` module implicitly. The generated C code may not compile. - enable_globals bool // allow __global for low level code - is_bare bool // set by -freestanding - bare_builtin_dir string // Set by -bare-builtin-dir xyz/ . The xyz/ module should contain implementations of malloc, memset, etc, that are used by the rest of V's `builtin` module. That option is only useful with -freestanding (i.e. when is_bare is true). - no_preludes bool // Prevents V from generating preludes in resulting .c files - custom_prelude string // Contents of custom V prelude that will be prepended before code in resulting .c files - no_closures bool // Produce a compile time error, if a closure was generated for any reason (an implicit receiver method was stored, or an explicit `fn [captured]()`). - cmain string // The name of the generated C main function. Useful with framework like code, that uses macros to re-define `main`, like SDL2 does. When set, V will always generate `int THE_NAME(int ___argc, char** ___argv){`, *no matter* the platform. - lookup_path []string - output_cross_c bool // true, when the user passed `-os cross` or `-cross` - output_es5 bool - prealloc bool - vroot string - vlib string // absolute path to the vlib/ folder - vmodules_paths []string // absolute paths to the vmodules folders, by default ['/home/user/.vmodules'], can be overridden by setting VMODULES - out_name_c string // full os.real_path to the generated .tmp.c file; set by builder. - out_name string - out_name_is_dir bool // true when `-o`/`-output` was passed with a trailing path separator - path string // Path to file/folder to compile - line_info string // `-line-info="file.v:28"`: for "mini VLS" (shows information about objects on provided line) - linfo LineInfo + no_builtin bool // Skip adding the `builtin` module implicitly. The generated C code may not compile. + enable_globals bool // allow __global for low level code + disable_explicit_mutability bool // allow ordinary variables to be mutated without explicit `mut` annotations + is_bare bool // set by -freestanding + bare_builtin_dir string // Set by -bare-builtin-dir xyz/ . The xyz/ module should contain implementations of malloc, memset, etc, that are used by the rest of V's `builtin` module. That option is only useful with -freestanding (i.e. when is_bare is true). + no_preludes bool // Prevents V from generating preludes in resulting .c files + custom_prelude string // Contents of custom V prelude that will be prepended before code in resulting .c files + no_closures bool // Produce a compile time error, if a closure was generated for any reason (an implicit receiver method was stored, or an explicit `fn [captured]()`). + cmain string // The name of the generated C main function. Useful with framework like code, that uses macros to re-define `main`, like SDL2 does. When set, V will always generate `int THE_NAME(int ___argc, char** ___argv){`, *no matter* the platform. + lookup_path []string + output_cross_c bool // true, when the user passed `-os cross` or `-cross` + output_es5 bool + prealloc bool + vroot string + vlib string // absolute path to the vlib/ folder + vmodules_paths []string // absolute paths to the vmodules folders, by default ['/home/user/.vmodules'], can be overridden by setting VMODULES + out_name_c string // full os.real_path to the generated .tmp.c file; set by builder. + out_name string + out_name_is_dir bool // true when `-o`/`-output` was passed with a trailing path separator + path string // Path to file/folder to compile + line_info string // `-line-info="file.v:28"`: for "mini VLS" (shows information about objects on provided line) + linfo LineInfo run_only []string // VTEST_ONLY_FN and -run-only accept comma separated glob patterns. exclude []string // glob patterns for excluding .v files from the list of .v files that otherwise would have been used for a compilation, example: `-exclude @vlib/math/*.c.v` @@ -621,6 +622,10 @@ pub fn parse_args_and_show_errors(known_external_commands []string, args []strin '-enable-globals' { res.enable_globals = true } + '--disable-explicit-mutability', '-disable-explicit-mutability' { + res.disable_explicit_mutability = true + res.build_options << arg + } '-autofree' { res.autofree = true res.gc_mode = .no_gc diff --git a/vlib/v/pref/pref_test.v b/vlib/v/pref/pref_test.v index ecf1572c3..ddbcc90af 100644 --- a/vlib/v/pref/pref_test.v +++ b/vlib/v/pref/pref_test.v @@ -52,6 +52,19 @@ fn test_cross_compile_keeps_explicit_cc() { assert second.ccompiler == custom_cc } +fn test_disable_explicit_mutability_flag() { + target := os.join_path(vroot, 'examples', 'hello_world.v') + prefs, _ := pref.parse_args_and_show_errors([], ['-disable-explicit-mutability', target], + false) + assert prefs.disable_explicit_mutability + assert prefs.build_options.contains('-disable-explicit-mutability') + + prefs2, _ := pref.parse_args_and_show_errors([], ['--disable-explicit-mutability', target], + false) + assert prefs2.disable_explicit_mutability + assert prefs2.build_options.contains('--disable-explicit-mutability') +} + fn new_wasm_preferences() pref.Preferences { return pref.Preferences{ backend: .wasm -- 2.39.5