From e8d07140a8d39ea7de6c8a81795534adf80a5f60 Mon Sep 17 00:00:00 2001 From: Vincenzo Palazzo Date: Tue, 31 Mar 2026 13:39:39 +0200 Subject: [PATCH] checker,cgen: allow embedded interface upcasts (#26772) Add support for implicit upcasting when an interface embeds another interface (including transitive chains like Dense -> Solid -> Base). - checker: `interface_embeds_interface_recursive` walks the embed tree to accept the upcast instead of erroring with "cannot implement interface with a different interface". - cgen: `expr_with_cast` emits `I__as_I_(...)` conversion calls for interface-to-interface casts, skipping the branch when either side carries `shared_f` so the existing shared-container path handles those. - cgen/fn: `v_typeof_interface_` helpers now return `string` (via `_S(...)`) instead of `char *`, fixing charptr/string mismatches. - vfmt/parser: preserve attribute call syntax (`@[attr(...)]`) through format round-trips. - closure: keep ppc64 bootstrap-compatible by guarding arch-specific trampoline code. Fixes #26759. Co-authored-by: Claude Opus 4.6 (1M context) --- vlib/builtin/closure/closure.c.v | 36 +++++++++++++------ vlib/v/ast/attr.v | 3 +- vlib/v/checker/checker.v | 34 ++++++++++++++++++ vlib/v/fmt/attrs.v | 4 ++- .../v/fmt/tests/attribute_call_syntax_keep.vv | 6 ++++ vlib/v/gen/c/cgen.v | 34 ++++++++++++++---- vlib/v/gen/c/fn.v | 8 +++-- vlib/v/parser/attribute.v | 25 ++++++++----- ...interface_embedding_implicit_upcast_test.v | 33 +++++++++++++++++ 9 files changed, 154 insertions(+), 29 deletions(-) create mode 100644 vlib/v/tests/interfaces/interface_embedding_implicit_upcast_test.v diff --git a/vlib/builtin/closure/closure.c.v b/vlib/builtin/closure/closure.c.v index 2ca090ea3..b17984fcd 100644 --- a/vlib/builtin/closure/closure.c.v +++ b/vlib/builtin/closure/closure.c.v @@ -5,13 +5,16 @@ module closure // https://nullprogram.com/blog/2017/01/08/ const assumed_page_size = int(0x4000) +const ppc64_architecture = int(11) + +type ClosureGetDataFn = fn () voidptr @[heap] struct Closure { ClosureMutex mut: closure_ptr voidptr - closure_get_data fn () voidptr = unsafe { nil } + closure_get_data ClosureGetDataFn = unsafe { nil } closure_cap int v_page_size int = int(0x4000) } @@ -23,10 +26,22 @@ enum MemoryProtectAtrr { read_write } +// Keep this runtime check bootstrap-compatible. Older compilers can not parse `$if ppc64` yet. +@[inline] +fn is_ppc64() bool { + $if big_endian { + return C.__V_architecture == ppc64_architecture + } $else { + return false + } +} + // refer to https://godbolt.org/z/r7P3EYv6c for a complete assembly // // NOTE: Keep the first branch as the longest byte sequence. In translated/bootstrap C mode // (`vc/v.c`), V emits a fixed C array whose size is inferred from the first branch. +// The final `big_endian` branch maps to ppc64 here, since the supported big-endian +// closure targets handled above are s390x and sparc64. // vfmt off pub const closure_thunk = $if ppc64le { [ @@ -108,7 +123,7 @@ pub const closure_thunk = $if ppc64le { 0x81, 0xc0, 0x40, 0x00, // jmp %g1 0x01, 0x00, 0x00, 0x00 // nop ]! -} $else $if ppc64 { +} $else $if big_endian { [ u8(0x7C), 0x08, 0x02, 0xA6, // mflr %r0 0x48, 0x00, 0x00, 0x05, // bl here @@ -181,7 +196,7 @@ const closure_get_data_bytes = $if arm32 { 0x81, 0xc3, 0xe0, 0x08, // retl 0x01, 0x00, 0x00, 0x00 // nop ]! -} $else $if ppc64 { +} $else $if big_endian { [ u8(0x7d), 0xc3, 0x00, 0x66, // mfvsrd %r3, %f14 0x4e, 0x80, 0x00, 0x20 // blr @@ -246,14 +261,14 @@ fn closure_init() { closure_memory_protect_platform(g_closure.closure_ptr, page_size, .read_exec) } // Setup global closure handler pointer - $if ppc64 { + if is_ppc64() { mut desc := unsafe { &voidptr(&u8(g_closure.closure_ptr) - assumed_page_size) } unsafe { desc[0] = g_closure.closure_ptr desc[1] = nil } - g_closure.closure_get_data = desc - } $else { + g_closure.closure_get_data = unsafe { ClosureGetDataFn(desc) } + } else { g_closure.closure_get_data = g_closure.closure_ptr } @@ -283,7 +298,7 @@ fn closure_create(func voidptr, data voidptr) voidptr { // Write closure metadata (data + function pointer) mut p := &voidptr(&u8(curr_closure) - assumed_page_size) - $if ppc64 { + if is_ppc64() { // ELFv1: guard page layout per slot: // [0] desc[0] = thunk code address <- returned as ELFv1 function pointer // [1] desc[1] = nil (TOC unused; thunk loads real TOC from func descriptor) @@ -293,7 +308,7 @@ fn closure_create(func voidptr, data voidptr) voidptr { p[1] = nil p[2] = data p[3] = func - } $else { + } else { p[0] = data // Stored closure context p[1] = func // Target function to execute } @@ -301,10 +316,9 @@ fn closure_create(func voidptr, data voidptr) voidptr { closure_mtx_unlock_platform() // Return executable closure object - $if ppc64 { + if is_ppc64() { // ELFv1: return descriptor address (guard page), not raw code address return unsafe { &u8(curr_closure) - assumed_page_size } - } $else { - return curr_closure } + return curr_closure } diff --git a/vlib/v/ast/attr.v b/vlib/v/ast/attr.v index c79fed2d3..1d08eff0a 100644 --- a/vlib/v/ast/attr.v +++ b/vlib/v/ast/attr.v @@ -28,6 +28,7 @@ pub: // original call-style metadata for `@[foo(...)]`, used by vfmt call_name string call_arg_name string + call_arg_idx int = -1 pub mut: ct_expr Expr // .kind == comptime_define, for [if !name] ct_evaled bool // whether ct_skip has been evaluated already @@ -35,7 +36,7 @@ pub mut: } pub fn (a &Attr) debug() string { - return 'Attr{ name: "${a.name}", has_arg: ${a.has_arg}, arg: "${a.arg}", kind: ${a.kind}, ct_expr: ${a.ct_expr}, ct_opt: ${a.ct_opt}, ct_skip: ${a.ct_skip}, call_name: "${a.call_name}", call_arg_name: "${a.call_arg_name}" }' + return 'Attr{ name: "${a.name}", has_arg: ${a.has_arg}, arg: "${a.arg}", kind: ${a.kind}, ct_expr: ${a.ct_expr}, ct_opt: ${a.ct_opt}, ct_skip: ${a.ct_skip}, call_name: "${a.call_name}", call_arg_name: "${a.call_arg_name}", call_arg_idx: ${a.call_arg_idx} }' } // str returns the string representation without square brackets diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index a9b1ced77..945c2291d 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -1404,6 +1404,9 @@ fn (mut c Checker) type_implements(typ ast.Type, interface_type ast.Type, pos to } if typ_sym.kind == .interface && inter_sym.kind == .interface && !styp.starts_with('JS.') && !inter_sym.name.starts_with('JS.') { + if c.interface_embeds_interface(utyp, interface_type) { + return true + } c.error('cannot implement interface `${inter_sym.name}` with a different interface `${styp}`', pos) } @@ -6197,6 +6200,37 @@ fn (mut c Checker) fail_if_unreadable(expr ast.Expr, typ ast.Type, what string) return false } +fn (mut c Checker) interface_embeds_interface(interface_type ast.Type, embedded_interface_type ast.Type) bool { + mut visited := map[int]bool{} + return c.interface_embeds_interface_recursive(interface_type, embedded_interface_type, mut + visited) +} + +fn (mut c Checker) interface_embeds_interface_recursive(interface_type ast.Type, embedded_interface_type ast.Type, mut visited map[int]bool) bool { + u_interface_type := c.unwrap_generic(interface_type) + if visited[u_interface_type.idx()] { + return false + } + visited[u_interface_type.idx()] = true + embedded_idx := c.unwrap_generic(embedded_interface_type).idx() + final_embedded_idx := c.table.final_sym(embedded_interface_type).idx + if iface_decl := c.table.interfaces[u_interface_type] { + for embed in iface_decl.embeds { + embed_typ := c.unwrap_generic(embed.typ) + if embed_typ.idx() == embedded_idx + || c.table.final_sym(embed_typ).idx == final_embedded_idx { + return true + } + if c.interface_embeds_interface_recursive(embed_typ, embedded_interface_type, mut + visited) + { + return true + } + } + } + return false +} + fn (mut c Checker) fail_if_stack_struct_action_outside_unsafe(mut ident ast.Ident, failed_action string) { if mut ident.obj is ast.Var { mut obj := unsafe { &ident.obj } diff --git a/vlib/v/fmt/attrs.v b/vlib/v/fmt/attrs.v index 83d6f4879..d1815f701 100644 --- a/vlib/v/fmt/attrs.v +++ b/vlib/v/fmt/attrs.v @@ -141,8 +141,10 @@ fn attr_call_group_str(attrs []ast.Attr) string { if attrs.len == 0 { return '' } + mut ordered_attrs := attrs.clone() + ordered_attrs.sort(a.call_arg_idx < b.call_arg_idx) mut args := []string{} - for attr in attrs { + for attr in ordered_attrs { if !attr.has_arg { continue } diff --git a/vlib/v/fmt/tests/attribute_call_syntax_keep.vv b/vlib/v/fmt/tests/attribute_call_syntax_keep.vv index ec2c8ddaf..291bfacf7 100644 --- a/vlib/v/fmt/tests/attribute_call_syntax_keep.vv +++ b/vlib/v/fmt/tests/attribute_call_syntax_keep.vv @@ -7,6 +7,12 @@ fn old_positional() {} @[deprecated(msg: 'use new_fn instead', after: '2999-10-10')] fn old_named() {} +@[foo(bar: 'x', 123)] +fn mixed_named_positional() {} + +@[foo(123, bar: 'x')] +fn mixed_positional_named() {} + struct Config { flag bool @[custom(flag: true, count: 2)] value string @[xml(name: 'cfg'); raw] diff --git a/vlib/v/gen/c/cgen.v b/vlib/v/gen/c/cgen.v index dab1e0ab3..9144a479d 100644 --- a/vlib/v/gen/c/cgen.v +++ b/vlib/v/gen/c/cgen.v @@ -1229,11 +1229,11 @@ pub fn (mut g Gen) write_typeof_functions() { continue } already_generated_ifaces[sym.cname] = true - g.definitions.writeln('${g.static_non_parallel}char * v_typeof_interface_${sym.cname}(u32 sidx);') + g.definitions.writeln('${g.static_non_parallel}string v_typeof_interface_${sym.cname}(u32 sidx);') if g.pref.parallel_cc { - g.extern_out.writeln('extern char * v_typeof_interface_${sym.cname}(u32 sidx);') + g.extern_out.writeln('extern string v_typeof_interface_${sym.cname}(u32 sidx);') } - g.writeln('${g.static_non_parallel}char * v_typeof_interface_${sym.cname}(u32 sidx) {') + g.writeln('${g.static_non_parallel}string v_typeof_interface_${sym.cname}(u32 sidx) {') for t in inter_info.types { sub_sym := g.table.sym(ast.mktyp(t)) if sub_sym.info is ast.Struct && sub_sym.info.is_unresolved_generic() { @@ -1243,9 +1243,9 @@ pub fn (mut g Gen) write_typeof_functions() { && sub_sym.idx !in g.table.used_features.used_syms { continue } - g.writeln('\tif (sidx == _${sym.cname}_${sub_sym.cname}_index) return "${util.strip_main_name(sub_sym.name)}";') + g.writeln('\tif (sidx == _${sym.cname}_${sub_sym.cname}_index) return _S("${util.strip_main_name(sub_sym.name)}");') } - g.writeln2('\treturn "unknown ${util.strip_main_name(sym.name)}";', '}') + g.writeln2('\treturn _S("unknown ${util.strip_main_name(sym.name)}");', '}') // Avoid duplicate symbol '_v_typeof_interface_idx_IError' when using -usecache if g.pref.build_mode != .build_module { g.definitions.writeln('u32 v_typeof_interface_idx_${sym.cname}(u32 sidx);') @@ -3314,6 +3314,28 @@ fn (mut g Gen) expr_with_cast(expr ast.Expr, got_type_raw ast.Type, expected_typ } return } + if got_sym.info is ast.Interface && exp_sym.info is ast.Interface + && got_type.idx() != expected_type.idx() && !got_type.has_flag(.shared_f) + && !expected_type.has_flag(.shared_f) { + g.write('I_${got_sym.cname}_as_I_${exp_sym.cname}(') + if got_type.is_ptr() { + g.write('*') + } + g.expr(expr) + g.write(')') + + mut got_interface_sym := g.table.sym(got_type) + mut info := got_interface_sym.info as ast.Interface + lock info.conversions { + if expected_type !in info.conversions { + left_variants := g.table.iface_types[got_sym.name] + right_variants := g.table.iface_types[exp_sym.name] + info.conversions[expected_type] = left_variants.filter(it in right_variants) + } + } + got_interface_sym.info = info + return + } // cast to sum type exp_styp := g.styp(expected_type) mut got_styp := g.styp(got_type) @@ -8717,7 +8739,7 @@ return ${cast_shared_struct_str}; variant_sym := g.table.sym(variant) conversion_functions.writeln('\tif (x._typ == _${interface_name}_${variant_sym.cname}_index) return I_${variant_sym.cname}_to_Interface_${vsym.cname}(x._${variant_sym.cname});') } - pmessage := 'builtin__string__plus(builtin__string__plus(builtin__tos3("`as_cast`: cannot convert "), builtin__tos3(v_typeof_interface_${interface_name}(x._typ))), builtin__tos3(" to ${util.strip_main_name(vsym.name)}"))' + pmessage := 'builtin__string__plus(builtin__string__plus(_S("`as_cast`: cannot convert "), v_typeof_interface_${interface_name}(x._typ)), _S(" to ${util.strip_main_name(vsym.name)}"))' if g.pref.is_debug { // TODO: actually return a valid position here conversion_functions.write_string2('\tbuiltin__panic_debug(1, builtin__tos3("builtin.v"), builtin__tos3("builtin"), builtin__tos3("__as_cast"), ', diff --git a/vlib/v/gen/c/fn.v b/vlib/v/gen/c/fn.v index 5c1249b41..dd9d127ed 100644 --- a/vlib/v/gen/c/fn.v +++ b/vlib/v/gen/c/fn.v @@ -1769,10 +1769,14 @@ fn (mut g Gen) method_call(node ast.CallExpr) { prefix_name := if left_sym.kind == .sum_type { 'sumtype' } else { 'interface' } match node.kind { .type_name { - if left_sym.kind in [.sum_type, .interface] { - g.conversion_function_call('builtin__charptr_vstring_literal(v_typeof_${prefix_name}_${typ_sym.cname}', + if left_sym.kind == .sum_type { + g.conversion_function_call('builtin__charptr_vstring_literal(v_typeof_sumtype_${typ_sym.cname}', ')', node) return + } else if left_sym.kind == .interface { + g.conversion_function_call('v_typeof_interface_${typ_sym.cname}', + '', node) + return } } .type_idx { diff --git a/vlib/v/parser/attribute.v b/vlib/v/parser/attribute.v index cc0f896a5..34edf6afc 100644 --- a/vlib/v/parser/attribute.v +++ b/vlib/v/parser/attribute.v @@ -43,10 +43,12 @@ fn (mut p Parser) parse_attr_call(name string, is_at bool, apos token.Pos) []ast mut base_arg := '' mut base_quote := u8(`'`) mut base_arg_name := '' + mut base_call_arg_idx := -1 mut base_has_arg := false mut attrs := []ast.Attr{} mut has_base_arg := false mut positional_arg_idx := 1 + mut call_arg_idx := 0 for p.tok.kind !in [.rpar, .eof] { mut is_named := false mut arg_name := '' @@ -68,6 +70,7 @@ fn (mut p Parser) parse_attr_call(name string, is_at bool, apos token.Pos) []ast base_kind = kind base_quote = quote base_arg_name = arg_name + base_call_arg_idx = call_arg_idx has_base_arg = true } else { attrs << ast.Attr{ @@ -80,6 +83,7 @@ fn (mut p Parser) parse_attr_call(name string, is_at bool, apos token.Pos) []ast has_at: is_at call_name: name call_arg_name: arg_name + call_arg_idx: call_arg_idx } } } else if !has_base_arg { @@ -87,20 +91,24 @@ fn (mut p Parser) parse_attr_call(name string, is_at bool, apos token.Pos) []ast base_arg = arg base_kind = kind base_quote = quote + base_arg_name = arg_name + base_call_arg_idx = call_arg_idx has_base_arg = true } else { attrs << ast.Attr{ - name: '${name}_${positional_arg_idx}' - has_arg: true - arg: arg - kind: kind - quote: quote - pos: apos.extend(p.prev_tok.pos()) - has_at: is_at - call_name: name + name: '${name}_${positional_arg_idx}' + has_arg: true + arg: arg + kind: kind + quote: quote + pos: apos.extend(p.prev_tok.pos()) + has_at: is_at + call_name: name + call_arg_idx: call_arg_idx } positional_arg_idx++ } + call_arg_idx++ if p.tok.kind == .comma { p.next() continue @@ -118,6 +126,7 @@ fn (mut p Parser) parse_attr_call(name string, is_at bool, apos token.Pos) []ast has_at: is_at call_name: name call_arg_name: base_arg_name + call_arg_idx: base_call_arg_idx } attrs.insert(0, base_attr) return attrs diff --git a/vlib/v/tests/interfaces/interface_embedding_implicit_upcast_test.v b/vlib/v/tests/interfaces/interface_embedding_implicit_upcast_test.v new file mode 100644 index 000000000..071bad0bb --- /dev/null +++ b/vlib/v/tests/interfaces/interface_embedding_implicit_upcast_test.v @@ -0,0 +1,33 @@ +interface Base { +} + +interface Solid { + Base +} + +interface Dense { + Solid +} + +struct Empty { +} + +fn greet(x Base) bool { + return x is Empty +} + +fn test_interface_embedding_implicit_upcast() { + solid := Solid(Empty{}) + assert greet(solid) + + mut base := Base(Empty{}) + base = solid + assert base is Empty + assert (solid as Base) is Empty + + dense := Dense(Empty{}) + assert greet(dense) + base = dense + assert base is Empty + assert (dense as Base) is Empty +} -- 2.39.5