From d480353ecbda7955a5910cbde7daa9146cb06003 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 24 Jun 2026 19:58:36 +0300 Subject: [PATCH] cgen: fix sumtype string-rvalue cast, ?&T == nil unwrap, and generic default interface field (#27551) --- vlib/v/gen/c/auto_str_methods.v | 9 +++ vlib/v/gen/c/cgen.v | 32 +++++++++- .../cast_string_rvalue_to_sumtype_test.v | 63 +++++++++++++++++++ ...eric_struct_default_interface_field_test.v | 29 +++++++++ .../option_ptr_field_nil_check_unwrap_test.v | 42 +++++++++++++ 5 files changed, 172 insertions(+), 3 deletions(-) create mode 100644 vlib/v/tests/casts/cast_string_rvalue_to_sumtype_test.v create mode 100644 vlib/v/tests/generics/generic_struct_default_interface_field_test.v create mode 100644 vlib/v/tests/options/option_ptr_field_nil_check_unwrap_test.v diff --git a/vlib/v/gen/c/auto_str_methods.v b/vlib/v/gen/c/auto_str_methods.v index ed1985e41..d0d6bf4c9 100644 --- a/vlib/v/gen/c/auto_str_methods.v +++ b/vlib/v/gen/c/auto_str_methods.v @@ -439,6 +439,15 @@ fn (mut g Gen) gen_str_for_interface(info ast.Interface, styp string, typ_str st fn_builder.writeln('${g.static_non_parallel}string indent_${str_fn_name}(${styp} x, ${ast.int_type_name} indent_count) { /* gen_str_for_interface */') fn_builder.writeln('\tif (x._typ == 0 && x._object == NULL) return _S("nil");') for typ in info.types { + // Skip unresolved generic struct variants (e.g. a leftover `Text[T]` + // registered when a generic struct is used as the default value of an + // interface-typed field in a generic wrapper). Only their concrete + // instantiations (`Text[int]`) are real runtime variants. This mirrors + // the same skip in interface_table(). + type_sym := g.table.sym(typ) + if type_sym.info is ast.Struct && type_sym.info.is_unresolved_generic() { + continue + } sub_sym := g.table.sym(ast.mktyp(typ)) if g.pref.skip_unused && sub_sym.idx !in g.table.used_features.used_syms { continue diff --git a/vlib/v/gen/c/cgen.v b/vlib/v/gen/c/cgen.v index 420dc2aac..0c583212b 100644 --- a/vlib/v/gen/c/cgen.v +++ b/vlib/v/gen/c/cgen.v @@ -5153,6 +5153,23 @@ fn (mut g Gen) fn_ptr_cast_typ(func ast.FnType) string { return g.fn_ptr_decl_str(func, ptr_name).replace_once(ptr_name, '') } +// expr_is_range_index_rvalue reports whether expr is rooted in a slice +// (`s[a..b]`). A slice yields a fresh rvalue with no stable address, so it (and +// anything reading through it, e.g. `(s[a..b]).len` or `arr[a..b][0].field`) +// must be materialized via ADDR rather than `&` in a sumtype cast. The recursion +// mirrors the lvalue-recursing shapes of `ast.Expr.is_lvalue()`, so that an +// expression `is_lvalue()` accepts is still caught when its root is a slice. +fn expr_is_range_index_rvalue(expr ast.Expr) bool { + return match expr { + ast.IndexExpr { expr.index is ast.RangeExpr || expr_is_range_index_rvalue(expr.left) } + ast.SelectorExpr { expr_is_range_index_rvalue(expr.expr) } + ast.ParExpr { expr_is_range_index_rvalue(expr.expr) } + ast.PrefixExpr { expr_is_range_index_rvalue(expr.right) } + ast.ComptimeSelector { expr_is_range_index_rvalue(expr.field_expr) } + else { false } + } +} + fn (mut g Gen) call_cfn_for_casting_expr(fname string, expr ast.Expr, exp ast.Type, got ast.Type, actual_got ast.Type, exp_styp string, got_is_ptr bool, got_is_fn bool, got_styp string) { mut rparen_n := 1 @@ -5174,7 +5191,11 @@ fn (mut g Gen) call_cfn_for_casting_expr(fname string, expr ast.Expr, exp ast.Ty } else if is_interface_cast { interface_cast_source_expr.is_lvalue() } else { - expr.is_lvalue() + // A slice expression (`s[a..b]`) yields a fresh rvalue with no stable + // address, even though `is_lvalue()` reports it (and selector/index + // reads rooted in it) as one. Treat anything rooted in a slice as an + // rvalue so the sumtype cast materializes it via ADDR instead of `&`. + expr.is_lvalue() && !expr_is_range_index_rvalue(expr) } is_comptime_variant := is_not_ptr_and_fn && expr is ast.Ident && g.comptime.is_comptime_variant_var(expr) @@ -7984,8 +8005,13 @@ fn (mut g Gen) selector_expr(node ast.SelectorExpr) { for i, typ in smartcasts { if i == 0 && (is_option_unwrap || nested_unwrap) { deref := if g.inside_selector { - if is_iface_or_sumtype || (field.orig_type.is_ptr() && g.left_is_opt - && is_option_unwrap) { + if is_iface_or_sumtype + || (field.orig_type.is_ptr() && is_option_unwrap) { + // Unwrapping an option-of-pointer field (`?&T`): the + // option's `.data` buffer holds a single `&T`, so one + // deref of the `(T**)` cast yields the pointer. This must + // hold for every unwrap form (smartcast `:=`, `== nil`, + // `== none`), independent of `g.left_is_opt`. '*'.repeat(typ.nr_muls()) } else { '*'.repeat(typ.nr_muls() + 1) diff --git a/vlib/v/tests/casts/cast_string_rvalue_to_sumtype_test.v b/vlib/v/tests/casts/cast_string_rvalue_to_sumtype_test.v new file mode 100644 index 000000000..75f5aac14 --- /dev/null +++ b/vlib/v/tests/casts/cast_string_rvalue_to_sumtype_test.v @@ -0,0 +1,63 @@ +// Casting a string rvalue (a slice or a concatenation) into a sumtype must +// materialize the rvalue into a temporary before taking its address, otherwise +// cgen emits `&` which strict C compilers reject. See issue #27548. +type Value = int | string + +fn slice(s string) Value { + return Value(s[1..3]) +} + +fn paren_slice(s string) Value { + // `is_lvalue()` recurses through parentheses, so a parenthesized slice must + // also be materialized via ADDR. + return Value((s[1..3])) +} + +fn slice_len(s string) Value { + // a selector read rooted in a slice rvalue (`(s[a..b]).len`) is still an + // rvalue; `is_lvalue()` recurses through selectors, so it must use ADDR too. + return Value((s[1..3]).len) +} + +struct Foo { + name string +} + +fn slice_elem_field(arr []Foo) Value { + // an index + selector read rooted in an array-slice rvalue. + return Value(arr[1..3][0].name) +} + +fn concat(a string, b string) Value { + return Value(a + b) +} + +fn test_string_slice_cast_to_sumtype() { + v := slice('hello') + assert v is string + assert (v as string) == 'el' +} + +fn test_string_paren_slice_cast_to_sumtype() { + v := paren_slice('hello') + assert v is string + assert (v as string) == 'el' +} + +fn test_int_slice_len_cast_to_sumtype() { + v := slice_len('hello') + assert v is int + assert (v as int) == 2 +} + +fn test_string_slice_elem_field_cast_to_sumtype() { + v := slice_elem_field([Foo{'a'}, Foo{'b'}, Foo{'c'}, Foo{'d'}]) + assert v is string + assert (v as string) == 'b' +} + +fn test_string_concat_cast_to_sumtype() { + v := concat('foo', 'bar') + assert v is string + assert (v as string) == 'foobar' +} diff --git a/vlib/v/tests/generics/generic_struct_default_interface_field_test.v b/vlib/v/tests/generics/generic_struct_default_interface_field_test.v new file mode 100644 index 000000000..ade1cfdfa --- /dev/null +++ b/vlib/v/tests/generics/generic_struct_default_interface_field_test.v @@ -0,0 +1,29 @@ +// A generic struct used as the default value of an interface-typed field inside +// a generic wrapper must monomorphize with the wrapper's type argument, instead +// of leaving the type parameter unresolved (`Text_T_T`). The leftover generic +// variant must also not pollute the interface's auto-generated str(). +// See issue #27550. +interface Tag { + tag() string +} + +struct Text[T] { + val T +} + +fn (t Text[T]) tag() string { + return 'text' +} + +struct Wrapper[T] { + tag Tag = Text[T]{} +} + +fn test_generic_struct_default_interface_field() { + w := Wrapper[int]{} + assert w.tag.tag() == 'text' + // stringifying the wrapper exercises the interface variant auto-str dispatch + s := w.str() + assert s.contains('Text[int]{') + assert s.contains('val: 0') +} diff --git a/vlib/v/tests/options/option_ptr_field_nil_check_unwrap_test.v b/vlib/v/tests/options/option_ptr_field_nil_check_unwrap_test.v new file mode 100644 index 000000000..a4004fdd5 --- /dev/null +++ b/vlib/v/tests/options/option_ptr_field_nil_check_unwrap_test.v @@ -0,0 +1,42 @@ +// Unwrapping an option-of-pointer struct field (`?&T`) after a `== nil` / +// `== none` check and accessing a member must emit a single dereference, the +// same as the smartcast (`if p := ... {}`) form. The option's `.data` buffer +// holds a single `&T`, so `*(T**)(data)` yields the pointer. See issue #27549. +struct SessionNode { + id string + parent ?&SessionNode +} + +fn parent_id_nil_check(node &SessionNode) string { + return unsafe { + if node.parent == nil { + 'root' + } else { + node.parent.id + } + } +} + +fn parent_id_smartcast(node &SessionNode) string { + if p := node.parent { + return p.id + } + return 'root' +} + +fn test_option_ptr_field_nil_check_unwrap() { + root := &SessionNode{ + id: 'root-node' + } + child := &SessionNode{ + id: 'child' + parent: root + } + + // the `== nil` ternary form must agree with the smartcast form + assert parent_id_nil_check(child) == 'root-node' + assert parent_id_nil_check(root) == 'root' + + assert parent_id_smartcast(child) == 'root-node' + assert parent_id_smartcast(root) == 'root' +} -- 2.39.5