From 6ab0d02922475efd48f572566fbd708d5e5122e7 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Tue, 21 Apr 2026 16:05:49 +0300 Subject: [PATCH] checker: fix option smartcast after non-option assignment (fixes #23611) --- vlib/v/checker/assign.v | 45 ++++++++++++++++++++ vlib/v/tests/options/option_if_unwrap_test.v | 15 +++++++ 2 files changed, 60 insertions(+) diff --git a/vlib/v/checker/assign.v b/vlib/v/checker/assign.v index 5a82da2e7..786dbe75c 100644 --- a/vlib/v/checker/assign.v +++ b/vlib/v/checker/assign.v @@ -78,6 +78,48 @@ fn (mut c Checker) smartcasted_assign_lhs_type(expr ast.Expr, fallback_type ast. return fallback_type } +fn (mut c Checker) reset_option_assignment_smartcast(mut expr ast.Ident, left_type ast.Type) { + if expr.scope == unsafe { nil } { + return + } + if var := expr.scope.find_var(expr.name) { + expr.scope.objects[expr.name] = ast.Var{ + ...var + typ: left_type + pos: var.pos + orig_type: ast.no_type + smartcasts: []ast.Type{} + is_unwrapped: false + } + } +} + +fn (mut c Checker) update_option_assignment_smartcast(mut expr ast.Expr, left_type ast.Type, right ast.Expr, right_type ast.Type) { + if !left_type.has_flag(.option) { + return + } + match mut expr { + ast.Ident { + mut original_pos := expr.pos + if var := expr.scope.find_var(expr.name) { + original_pos = var.pos + } + if right_type == ast.none_type || right_type == ast.nil_type + || right_type.has_flag(.option) || right.is_nil() + || (right is ast.UnsafeExpr && right.expr.is_nil()) { + c.reset_option_assignment_smartcast(mut expr, left_type) + return + } + c.smartcast(mut expr, left_type, left_type.clear_flag(.option), mut expr.scope, false, + true, false, true) + if mut scope_var := expr.scope.find_var(expr.name) { + scope_var.pos = original_pos + } + } + else {} + } +} + // TODO: 980 line function fn (mut c Checker) assign_stmt(mut node ast.AssignStmt) { prev_inside_assign := c.inside_assign @@ -1174,6 +1216,9 @@ or use an explicit `unsafe{ a[..] }`, if you do not want a copy of the slice.', if right_sym.kind == .alias && right_sym.name == 'byte' { c.error('byte is deprecated, use u8 instead', right.pos()) } + if original_op == .assign { + c.update_option_assignment_smartcast(mut left, left_type, right, right_type) + } } // this needs to run after the assign stmt left exprs have been run through checker // so that ident.obj is set diff --git a/vlib/v/tests/options/option_if_unwrap_test.v b/vlib/v/tests/options/option_if_unwrap_test.v index 7e41fe191..14835d4b4 100644 --- a/vlib/v/tests/options/option_if_unwrap_test.v +++ b/vlib/v/tests/options/option_if_unwrap_test.v @@ -20,6 +20,11 @@ pub mut: f ?[3]u8 } +struct Issue23611Foo { +mut: + test bool +} + fn unwrap_field(opt Opt) { if opt.f == none { println('${@FN}:${@LINE}: ${typeof(opt.f).name}') @@ -52,3 +57,13 @@ fn test_main() { f.f = [3]u8{init: 2} unwrap_field(f) } + +fn test_option_smartcast_after_non_option_assignment() { + mut foo := ?Issue23611Foo(none) + if foo == none { + foo = Issue23611Foo{} + foo.test = true + } + assert foo != none + assert foo?.test +} -- 2.39.5