From 298640c173b261175bf4335cb9318f6e1d9470a0 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 25 Mar 2026 16:42:26 +0300 Subject: [PATCH] checker: fix consecutive asserts ignoring previous (fixes #24194) --- vlib/v/checker/assign.v | 6 + vlib/v/checker/checker.v | 143 ++++++++++++++++++++++ vlib/v/checker/fn.v | 3 + vlib/v/tests/assign/assert_sumtype_test.v | 15 ++- 4 files changed, 166 insertions(+), 1 deletion(-) diff --git a/vlib/v/checker/assign.v b/vlib/v/checker/assign.v index 25a716313..3743ba000 100644 --- a/vlib/v/checker/assign.v +++ b/vlib/v/checker/assign.v @@ -189,6 +189,12 @@ fn (mut c Checker) assign_stmt(mut node ast.AssignStmt) { return } for i, mut left in node.left { + if !is_decl { + if left is ast.Ident { + ident := left as ast.Ident + c.clear_assert_autocast(ident.scope, ident.name) + } + } if mut left is ast.CallExpr { // ban `foo() = 10` if c.pref.is_vls { diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index 9f31b7056..d83d46479 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -42,6 +42,11 @@ pub const reserved_type_names = ['bool', 'char', 'i8', 'i16', 'i32', 'int', 'i64 pub const reserved_type_names_chk = token.new_keywords_matcher_from_array_trie(reserved_type_names) pub const vroot_is_deprecated_message = '@VROOT is deprecated, use @VMODROOT or @VEXEROOT instead' +struct AssertAutocast { + from_type ast.Type + to_type ast.Type +} + @[heap; minify] pub struct Checker { pub mut: @@ -140,6 +145,7 @@ mut: inside_decl_rhs bool inside_if_guard bool // true inside the guard condition of `if x := opt() {}` inside_assign bool + assert_autocasts map[string]AssertAutocast is_js_backend bool // doing_line_info int // a quick single file run when called with v -line-info (contains line nr to inspect) // doing_line_path string // same, but stores the path being parsed @@ -3437,6 +3443,11 @@ fn (mut c Checker) defer_stmt(mut node ast.DeferStmt) { fn (mut c Checker) assert_stmt(mut node ast.AssertStmt) { cur_exp_typ := c.expected_type c.expected_type = ast.bool_type + if !isnil(c.fn_scope) { + scope := c.fn_scope.innermost(node.pos.pos) + c.apply_assert_autocasts(mut node.expr, scope) + } + nr_errors_before := c.nr_errors assert_type := c.check_expr_option_or_result_call(node.expr, c.expr(mut node.expr)) c.markused_assertstmt_auto_str(mut node) if assert_type != ast.bool_type_idx { @@ -3453,6 +3464,12 @@ fn (mut c Checker) assert_stmt(mut node ast.AssertStmt) { } } c.fail_if_unreadable(node.expr, ast.bool_type_idx, 'assertion') + // Asserts are stripped in `-prod`, so only persist narrowing when the assert stays in the program. + if node.is_used && !isnil(c.fn_scope) && assert_type == ast.bool_type_idx + && c.nr_errors == nr_errors_before { + scope := c.fn_scope.innermost(node.pos.pos) + c.remember_assert_autocasts(mut node.expr, scope) + } c.expected_type = cur_exp_typ } @@ -6051,6 +6068,132 @@ fn smartcast_index_expr_scope_key(expr ast.IndexExpr) string { return '__smartcast_index_expr__${ast.Expr(expr).str()}' } +fn assert_autocast_scope_key(scope &ast.Scope, name string) string { + return '${scope.start_pos}:${scope.end_pos}:${name}' +} + +fn (c &Checker) find_assert_autocast(scope &ast.Scope, name string) ?AssertAutocast { + for sc := unsafe { scope }; sc != unsafe { nil }; sc = sc.parent { + if autocast := c.assert_autocasts[assert_autocast_scope_key(sc, name)] { + return autocast + } + if sc.parent == unsafe { nil } || sc.detached_from_parent { + break + } + } + return none +} + +fn (mut c Checker) clear_assert_autocast(scope &ast.Scope, name string) { + for sc := unsafe { scope }; sc != unsafe { nil }; sc = sc.parent { + key := assert_autocast_scope_key(sc, name) + if key in c.assert_autocasts { + c.assert_autocasts.delete(key) + return + } + if sc.parent == unsafe { nil } || sc.detached_from_parent { + break + } + } +} + +fn (mut c Checker) apply_assert_autocasts(mut expr ast.Expr, scope &ast.Scope) { + if expr is ast.Ident { + ident := expr as ast.Ident + ident_scope := if !isnil(ident.scope) { ident.scope } else { scope } + if autocast := c.find_assert_autocast(ident_scope, ident.name) { + expr = ast.Expr(ast.ParExpr{ + expr: ast.Expr(ast.AsCast{ + typ: autocast.to_type + expr: ast.Expr(ident) + expr_type: autocast.from_type + }) + }) + } + return + } + match mut expr { + ast.SelectorExpr { + c.apply_assert_autocasts(mut expr.expr, scope) + } + ast.ParExpr { + c.apply_assert_autocasts(mut expr.expr, scope) + } + ast.PrefixExpr { + c.apply_assert_autocasts(mut expr.right, scope) + } + ast.CallExpr { + if expr.left !is ast.Ident { + c.apply_assert_autocasts(mut expr.left, scope) + } + for mut arg in expr.args { + c.apply_assert_autocasts(mut arg.expr, scope) + } + } + ast.InfixExpr { + c.apply_assert_autocasts(mut expr.left, scope) + c.apply_assert_autocasts(mut expr.right, scope) + } + ast.IndexExpr { + c.apply_assert_autocasts(mut expr.left, scope) + c.apply_assert_autocasts(mut expr.index, scope) + } + ast.RangeExpr { + c.apply_assert_autocasts(mut expr.low, scope) + c.apply_assert_autocasts(mut expr.high, scope) + } + ast.StringInterLiteral { + for mut subexpr in expr.exprs { + c.apply_assert_autocasts(mut subexpr, scope) + } + } + ast.UnsafeExpr { + c.apply_assert_autocasts(mut expr.expr, scope) + } + ast.Likely { + c.apply_assert_autocasts(mut expr.expr, scope) + } + else {} + } +} + +fn (mut c Checker) remember_assert_autocasts(mut node ast.Expr, scope &ast.Scope) { + match mut node { + ast.InfixExpr { + if node.op == .and { + c.remember_assert_autocasts(mut node.left, scope) + c.remember_assert_autocasts(mut node.right, scope) + } else { + left := node.left + if left is ast.Ident && node.op == .ne && node.right is ast.None { + if node.left_type.has_flag(.option) { + autocast_scope := if !isnil(left.scope) { left.scope } else { scope } + + c.assert_autocasts[assert_autocast_scope_key(autocast_scope, left.name)] = AssertAutocast{ + from_type: node.left_type + to_type: node.left_type.clear_flag(.option) + } + } + } else if left is ast.Ident && node.op == .key_is { + autocast_scope := if !isnil(left.scope) { left.scope } else { scope } + + c.assert_autocasts[assert_autocast_scope_key(autocast_scope, left.name)] = AssertAutocast{ + from_type: node.left_type + to_type: node.right_type + } + } + } + } + ast.Likely { + c.remember_assert_autocasts(mut node.expr, scope) + } + ast.ParExpr { + c.remember_assert_autocasts(mut node.expr, scope) + } + else {} + } +} + // smartcast takes the expression with the current type which should be smartcasted to the target type in the given scope fn (mut c Checker) smartcast(mut expr ast.Expr, cur_type ast.Type, to_type_ ast.Type, mut scope ast.Scope, is_comptime bool, is_option_unwrap bool, allow_mut_selector_smartcast bool) { diff --git a/vlib/v/checker/fn.v b/vlib/v/checker/fn.v index 9d3adb835..3292c82bf 100644 --- a/vlib/v/checker/fn.v +++ b/vlib/v/checker/fn.v @@ -319,11 +319,13 @@ fn (mut c Checker) fn_decl(mut node ast.FnDecl) { prev_inside_anon_fn := c.inside_anon_fn prev_returns := c.returns prev_stmt_level := c.stmt_level + prev_assert_autocasts := c.assert_autocasts.clone() c.fn_level++ c.in_for_count = 0 c.inside_defer = false c.inside_unsafe = node.is_unsafe c.returns = false + c.assert_autocasts = map[string]AssertAutocast{} defer { c.stmt_level = prev_stmt_level c.fn_level-- @@ -333,6 +335,7 @@ fn (mut c Checker) fn_decl(mut node ast.FnDecl) { c.inside_defer = prev_inside_defer c.in_for_count = prev_in_for_count c.fn_scope = prev_fn_scope + c.assert_autocasts = prev_assert_autocasts.clone() } // Check generics fn/method without generic type parameters if node.language == .v && !c.is_builtin_mod && !node.is_anon { diff --git a/vlib/v/tests/assign/assert_sumtype_test.v b/vlib/v/tests/assign/assert_sumtype_test.v index b0445a262..df71f0bd2 100644 --- a/vlib/v/tests/assign/assert_sumtype_test.v +++ b/vlib/v/tests/assign/assert_sumtype_test.v @@ -4,8 +4,21 @@ struct Mars {} type World = Mars | Moon +interface Val {} + +const expected_val = 'cool' + +fn get_val() Val { + return expected_val +} + fn test_assert_sumtype() { w := World(Moon{}) assert w is Moon - assert w !is Mars +} + +fn test_consecutive_assert_smartcast() { + v := get_val() + assert v is string + assert v == expected_val } -- 2.39.5