Skip to content

Commit 5d00023

Browse files
committed
cgen: fix sumtype string-rvalue cast, ?&T == nil unwrap, and generic default interface field
Fixes three cgen bugs: - #27548: casting a string rvalue (slice `s[a..b]` or concat) into a sumtype emitted `&<rvalue>`, which strict C compilers reject. `is_lvalue()` reports a slice as an lvalue, so the cast took a bare address instead of materializing it. Treat a slice as an rvalue so the sumtype cast uses the ADDR macro. - #27549: unwrapping an option-of-pointer field (`?&T`) via a `== nil`/`== none` check emitted one too many dereferences (`**(T**)(data)` instead of `*(T**)(data)`). The single-deref path was gated on `g.left_is_opt`, which is only set in the smartcast (`:=`) form. The option's `.data` buffer holds a single `&T`, so one deref is always correct for `?&T`, independent of that flag. - #27550: a generic struct used as the default value of an interface-typed field in a generic wrapper left a leftover unresolved-generic variant (`Text[T]`) in the interface's types list, which the interface auto-str dispatch emitted as broken C (`Text_T_T`, undeclared `_..._index`). Skip unresolved-generic struct variants in gen_str_for_interface, mirroring the existing skip in interface_table(). The struct init itself already monomorphizes correctly.
1 parent d56f20f commit 5d00023

5 files changed

Lines changed: 115 additions & 3 deletions

File tree

vlib/v/gen/c/auto_str_methods.v

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,15 @@ fn (mut g Gen) gen_str_for_interface(info ast.Interface, styp string, typ_str st
439439
fn_builder.writeln('${g.static_non_parallel}string indent_${str_fn_name}(${styp} x, ${ast.int_type_name} indent_count) { /* gen_str_for_interface */')
440440
fn_builder.writeln('\tif (x._typ == 0 && x._object == NULL) return _S("nil");')
441441
for typ in info.types {
442+
// Skip unresolved generic struct variants (e.g. a leftover `Text[T]`
443+
// registered when a generic struct is used as the default value of an
444+
// interface-typed field in a generic wrapper). Only their concrete
445+
// instantiations (`Text[int]`) are real runtime variants. This mirrors
446+
// the same skip in interface_table().
447+
type_sym := g.table.sym(typ)
448+
if type_sym.info is ast.Struct && type_sym.info.is_unresolved_generic() {
449+
continue
450+
}
442451
sub_sym := g.table.sym(ast.mktyp(typ))
443452
if g.pref.skip_unused && sub_sym.idx !in g.table.used_features.used_syms {
444453
continue

vlib/v/gen/c/cgen.v

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5174,7 +5174,10 @@ fn (mut g Gen) call_cfn_for_casting_expr(fname string, expr ast.Expr, exp ast.Ty
51745174
} else if is_interface_cast {
51755175
interface_cast_source_expr.is_lvalue()
51765176
} else {
5177-
expr.is_lvalue()
5177+
// A slice expression (`s[a..b]`) yields a fresh rvalue with no stable
5178+
// address, even though `is_lvalue()` reports it as one. Treat it as an
5179+
// rvalue so the sumtype cast materializes it via ADDR instead of `&`.
5180+
expr.is_lvalue() && !(expr is ast.IndexExpr && expr.index is ast.RangeExpr)
51785181
}
51795182
is_comptime_variant := is_not_ptr_and_fn && expr is ast.Ident
51805183
&& g.comptime.is_comptime_variant_var(expr)
@@ -7984,8 +7987,13 @@ fn (mut g Gen) selector_expr(node ast.SelectorExpr) {
79847987
for i, typ in smartcasts {
79857988
if i == 0 && (is_option_unwrap || nested_unwrap) {
79867989
deref := if g.inside_selector {
7987-
if is_iface_or_sumtype || (field.orig_type.is_ptr() && g.left_is_opt
7988-
&& is_option_unwrap) {
7990+
if is_iface_or_sumtype
7991+
|| (field.orig_type.is_ptr() && is_option_unwrap) {
7992+
// Unwrapping an option-of-pointer field (`?&T`): the
7993+
// option's `.data` buffer holds a single `&T`, so one
7994+
// deref of the `(T**)` cast yields the pointer. This must
7995+
// hold for every unwrap form (smartcast `:=`, `== nil`,
7996+
// `== none`), independent of `g.left_is_opt`.
79897997
'*'.repeat(typ.nr_muls())
79907998
} else {
79917999
'*'.repeat(typ.nr_muls() + 1)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Casting a string rvalue (a slice or a concatenation) into a sumtype must
2+
// materialize the rvalue into a temporary before taking its address, otherwise
3+
// cgen emits `&<rvalue>` which strict C compilers reject. See issue #27548.
4+
type Value = int | string
5+
6+
fn slice(s string) Value {
7+
return Value(s[1..3])
8+
}
9+
10+
fn concat(a string, b string) Value {
11+
return Value(a + b)
12+
}
13+
14+
fn test_string_slice_cast_to_sumtype() {
15+
v := slice('hello')
16+
assert v is string
17+
assert (v as string) == 'el'
18+
}
19+
20+
fn test_string_concat_cast_to_sumtype() {
21+
v := concat('foo', 'bar')
22+
assert v is string
23+
assert (v as string) == 'foobar'
24+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// A generic struct used as the default value of an interface-typed field inside
2+
// a generic wrapper must monomorphize with the wrapper's type argument, instead
3+
// of leaving the type parameter unresolved (`Text_T_T`). The leftover generic
4+
// variant must also not pollute the interface's auto-generated str().
5+
// See issue #27550.
6+
interface Tag {
7+
tag() string
8+
}
9+
10+
struct Text[T] {
11+
val T
12+
}
13+
14+
fn (t Text[T]) tag() string {
15+
return 'text'
16+
}
17+
18+
struct Wrapper[T] {
19+
tag Tag = Text[T]{}
20+
}
21+
22+
fn test_generic_struct_default_interface_field() {
23+
w := Wrapper[int]{}
24+
assert w.tag.tag() == 'text'
25+
// stringifying the wrapper exercises the interface variant auto-str dispatch
26+
s := w.str()
27+
assert s.contains('Text[int]{')
28+
assert s.contains('val: 0')
29+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Unwrapping an option-of-pointer struct field (`?&T`) after a `== nil` /
2+
// `== none` check and accessing a member must emit a single dereference, the
3+
// same as the smartcast (`if p := ... {}`) form. The option's `.data` buffer
4+
// holds a single `&T`, so `*(T**)(data)` yields the pointer. See issue #27549.
5+
struct SessionNode {
6+
id string
7+
parent ?&SessionNode
8+
}
9+
10+
fn parent_id_nil_check(node &SessionNode) string {
11+
return unsafe {
12+
if node.parent == nil {
13+
'root'
14+
} else {
15+
node.parent.id
16+
}
17+
}
18+
}
19+
20+
fn parent_id_smartcast(node &SessionNode) string {
21+
if p := node.parent {
22+
return p.id
23+
}
24+
return 'root'
25+
}
26+
27+
fn test_option_ptr_field_nil_check_unwrap() {
28+
root := &SessionNode{
29+
id: 'root-node'
30+
}
31+
child := &SessionNode{
32+
id: 'child'
33+
parent: root
34+
}
35+
36+
// the `== nil` ternary form must agree with the smartcast form
37+
assert parent_id_nil_check(child) == 'root-node'
38+
assert parent_id_nil_check(root) == 'root'
39+
40+
assert parent_id_smartcast(child) == 'root-node'
41+
assert parent_id_smartcast(root) == 'root'
42+
}

0 commit comments

Comments
 (0)