From 493890af91c07e6190f5cc63ab8a05c7bb528000 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 29 Apr 2026 00:15:18 +0300 Subject: [PATCH] cgen: interface smartcast fix --- vlib/v/gen/c/cgen.v | 10 +- vlib/v/gen/c/utils.v | 7 ++ .../interface_smartcast_field_access_test.v | 116 ++++++++++++++++++ 3 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 vlib/v/tests/interfaces/interface_smartcast_field_access_test.v diff --git a/vlib/v/gen/c/cgen.v b/vlib/v/gen/c/cgen.v index 31f3403f7..3c5bab274 100644 --- a/vlib/v/gen/c/cgen.v +++ b/vlib/v/gen/c/cgen.v @@ -4874,8 +4874,8 @@ fn (mut g Gen) expr_with_cast(expr ast.Expr, got_type_raw ast.Type, expected_typ } } // Generic dereferencing logic - neither_void := ast.voidptr_type !in [got_type, expected_type] - && ast.nil_type !in [got_type, expected_type] + neither_void := ast.voidptr_type !in [got_type.idx_type(), expected_type.idx_type()] + && ast.nil_type !in [got_type.idx_type(), expected_type.idx_type()] if expected_type.has_flag(.shared_f) && !got_type_raw.has_flag(.shared_f) && !expected_type.has_option_or_result() { shared_styp := exp_styp[0..exp_styp.len - 1] // `shared` implies ptr, so eat one `*` @@ -7198,8 +7198,14 @@ fn (mut g Gen) selector_expr(node ast.SelectorExpr) { || (is_interface_smartcast_expr && g.table.final_sym(g.unwrap_generic(smartcast_expr_var.smartcasts.last())).kind != .interface && exposed_interface_smartcast_type.is_ptr()) + // Interface→interface smartcast: the conversion `I_X_as_I_Y(parent)` returns + // a struct value, not a pointer, so field access must use `.`, not `->`. + is_interface_to_interface_smartcast := is_interface_smartcast_expr + && g.table.final_sym(g.unwrap_generic(smartcast_expr_var.smartcasts.last())).kind == .interface left_is_ptr := if expr_is_unwrapped_autoheap_option { false + } else if is_interface_to_interface_smartcast { + false } else { field_is_opt || expr_is_auto_heap || interface_smartcast_selector_emits_ptr || (is_interface_smartcast_lhs && !interface_smartcast_expr_is_dereferenced diff --git a/vlib/v/gen/c/utils.v b/vlib/v/gen/c/utils.v index 96e6af7a7..70d29aca0 100644 --- a/vlib/v/gen/c/utils.v +++ b/vlib/v/gen/c/utils.v @@ -1169,6 +1169,13 @@ fn (mut g Gen) resolved_expr_type(expr ast.Expr, default_typ ast.Type) ast.Type } } ast.SelectorExpr { + // If this selector has been smart-cast in the current scope (e.g. + // `if mut w.face is X { ... w.face ... }`), use the smart-cast type + // rather than the field's declared type. + smartcast_typ := g.resolve_selector_smartcast_type(expr) + if smartcast_typ != 0 { + return smartcast_typ + } left_default := if expr.expr_type != 0 { expr.expr_type } else { default_typ } left_type := g.recheck_concrete_type(g.resolved_expr_type(expr.expr, left_default)) if left_type != 0 { diff --git a/vlib/v/tests/interfaces/interface_smartcast_field_access_test.v b/vlib/v/tests/interfaces/interface_smartcast_field_access_test.v new file mode 100644 index 000000000..abaf9a56d --- /dev/null +++ b/vlib/v/tests/interfaces/interface_smartcast_field_access_test.v @@ -0,0 +1,116 @@ +// Tests for interface smart-cast cgen bugs in field access and variable +// declaration paths. +// +// 1. Interface→interface smart-cast: `parent.id` where parent is smart-cast +// from one interface to another. Cgen used to emit +// `(I_X_as_I_Y(parent)->id)` (`->` on a struct value) instead of +// `(I_X_as_I_Y(parent).id)`. +// +// 2. Variable declared from a smart-cast selector should take the smart-cast +// type, not the original interface. `mut dd := w.face` inside +// `if mut w.face is Concrete` was being typed as the original interface, +// so embedded-field access (e.g. `dd.context.x`) crashed in cgen. +// +// 3. Auto-deref of a `mut` method receiver passed as `voidptr` to a generic +// method was emitting `*f` instead of `f` because the voidptr parameter +// type carried the `.generic` flag during cgen, defeating the +// `voidptr_type !in [...]` early-return. +import eventbus + +// ── (1) interface→interface smart-cast field access ──────────────────────── + +interface Foo { + id string +} + +interface Bar { + id string +} + +struct ImplFB { + id string +} + +fn check_iface_to_iface(parent Foo) string { + if parent is Bar { + return parent.id + } + return '' +} + +fn test_interface_to_interface_smartcast_field_access() { + i := &ImplFB{ + id: 'iface_to_iface' + } + assert check_iface_to_iface(i) == 'iface_to_iface' +} + +// ── (2) selector smart-cast var declaration ──────────────────────────────── + +struct Inner { + value string +} + +struct ConcreteWithEmbed { + Inner + tag string +} + +interface IFace {} + +struct WrapIF { +mut: + face IFace +} + +fn check_selector_smartcast_var(mut w WrapIF) string { + if mut w.face is ConcreteWithEmbed { + // `dd` should be ConcreteWithEmbed, not IFace, so accessing the + // embedded `Inner.value` field works. + dd := w.face + return '${dd.tag}:${dd.value}' + } + return '' +} + +fn test_selector_smartcast_var_declaration() { + mut c := ConcreteWithEmbed{ + Inner: Inner{ + value: 'inner_v' + } + tag: 'tag_v' + } + mut w := WrapIF{ + face: &c + } + assert check_selector_smartcast_var(mut w) == 'tag_v:inner_v' +} + +// ── (3) auto-deref of mut receiver passed as voidptr to a generic method ─── + +struct Counter { +mut: + got_self int + hits int +} + +fn counter_handler(receiver voidptr, args voidptr, sender voidptr) { + mut c := unsafe { &Counter(receiver) } + c.hits++ +} + +fn (mut c Counter) wire(mut bus eventbus.EventBus[string]) { + // Bare `c` here. The receiver is a `voidptr` parameter on a generic + // method, and the bug emitted `*c` (struct value) for the C call. + bus.subscriber.subscribe_method('hit', counter_handler, c) + c.got_self = unsafe { int(i64(voidptr(c)) & 0xffffffff) } +} + +fn test_voidptr_mut_receiver_in_generic_method() { + mut c := &Counter{} + mut bus := eventbus.new[string]() + c.wire(mut bus) + bus.publish('hit', unsafe { nil }, unsafe { nil }) + bus.publish('hit', unsafe { nil }, unsafe { nil }) + assert c.hits == 2 +} -- 2.39.5