From f882c8174f24df66dd40c8cbb2d14bd503ee9937 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sat, 11 Apr 2026 18:18:45 +0300 Subject: [PATCH] builtin: show file and line number in backtraces; v2: `implements`, clone() auto generation --- cmd/v/v.v | 8 +- vlib/builtin/backtraces.c.v | 81 +++++++++ vlib/builtin/backtraces_nix.c.v | 102 ++++++++++- vlib/builtin/backtraces_windows.c.v | 2 +- vlib/builtin/builtin_backtraces_nix.c.v | 6 + vlib/builtin/builtin_d_use_libbacktrace.c.v | 2 +- vlib/builtin/string_d_ownership.v | 6 + vlib/v/gen/c/fn.v | 7 +- vlib/v2/ast/ast.v | 1 + vlib/v2/ast_dump/ast_dump.v | 5 + vlib/v2/builder/cache_headers.v | 1 + vlib/v2/gen/v/gen.v | 9 + vlib/v2/parser/parser.v | 16 +- vlib/v2/transformer/fn.v | 10 ++ vlib/v2/transformer/transformer.v | 181 ++++++++++++++++++++ vlib/v2/transformer/transformer_test.v | 84 +++++++++ vlib/v2/transformer/types.v | 85 +++++++++ vlib/v2/types/checker.v | 72 ++++++-- vlib/v2/types/checker_ownership.v | 8 +- vlib/v2/types/checker_test.v | 16 ++ vlib/v2/types/types.v | 1 + 21 files changed, 672 insertions(+), 31 deletions(-) diff --git a/cmd/v/v.v b/cmd/v/v.v index 9c3f1d1d8..777629646 100644 --- a/cmd/v/v.v +++ b/cmd/v/v.v @@ -207,14 +207,14 @@ fn invoke_help_and_exit(remaining []string) { } fn maybe_delegate_to_v2(command string, prefs &pref.Preferences) { - if !prefs.use_v2 { + is_ownership := '-ownership' in os.args + if !prefs.use_v2 && !is_ownership { return } if !is_v2_relevant_command(command, prefs) { - eprintln('v: `-v2` currently supports direct compilation only. Use `v -v2 hello.v` or `v -v2 -b arm64 hello.v`.') + eprintln('v: `-v2`/`-ownership` currently support direct compilation only. Use `v -v2 hello.v` or `v -ownership module_dir`.') exit(1) } - is_ownership := '-ownership' in os.args launch_v2_compiler(prefs.is_verbose, os.args[1..].filter(it != '-v2'), is_ownership) } @@ -222,7 +222,7 @@ fn is_v2_relevant_command(command string, prefs &pref.Preferences) bool { if prefs.path == '' || prefs.is_run || prefs.is_crun { return false } - return command.ends_with('.v') && prefs.path.ends_with('.v') + return prefs.path == command && (command.ends_with('.v') || os.exists(command)) } @[noreturn] diff --git a/vlib/builtin/backtraces.c.v b/vlib/builtin/backtraces.c.v index 4c3a77405..18e185282 100644 --- a/vlib/builtin/backtraces.c.v +++ b/vlib/builtin/backtraces.c.v @@ -23,6 +23,87 @@ pub fn print_backtrace() { } } +// demangle_v_symbol converts a C-mangled V symbol name to a human-readable V name. +// For example: `veb__run_T_main__App_main__Context` => `veb.run[main.App, main.Context]` +@[direct_array_access] +fn demangle_v_symbol(cname string) string { + mut name := cname + // Strip builtin__ prefix: + if name.starts_with('builtin__') { + name = name[9..] + } + // Replace pointer type encoding: + name = name.replace('__ptr__', '&') + // Split base name from generic parameters on `_T_`: + // C mangling: `base_T_type1_type2` where each type is prefixed with `_` + // and types may contain `__` for module separators. + t_pos := name.index('_T_') or { -1 } + if t_pos >= 0 { + base := name[..t_pos].replace('__', '.') + generic_suffix := name[t_pos + 3..] + // Split on single `_` that is not part of `__` (double underscore). + params := split_generic_params(generic_suffix) + mut demangled_params := []string{cap: params.len} + for param in params { + demangled_params << param.replace('__', '.') + } + return base + '[' + demangled_params.join(', ') + ']' + } + // No generics - just replace module separator: + name = name.replace('__', '.') + // `main.main` is V's internal C mangling, in V it's just `main`: + if name == 'main.main' { + return 'main' + } + return name +} + +// split_generic_params splits a generic suffix like `main__App_main__Context` +// into individual type parameters [`main__App`, `main__Context`]. +// Splits on single `_` but not `__` (double underscore used for module separators). +@[direct_array_access] +fn split_generic_params(s string) []string { + mut params := []string{} + mut start := 0 + mut i := 0 + for i < s.len { + if s[i] == `_` { + if i + 1 < s.len && s[i + 1] == `_` { + // Part of `__` (module separator), skip both. + i += 2 + } else { + // Single `_` is a generic param separator. + if i > start { + params << s[start..i] + } + i++ + start = i + } + } else { + i++ + } + } + if start < s.len { + params << s[start..] + } + return params +} + +// demangle_backtrace_sym demangles a V symbol within a Linux-style backtrace fragment. +// Linux format: `./executable(symbol_name+0x1a4) ` +fn demangle_backtrace_sym(s string) string { + paren_start := s.index('(') or { return s } + plus_pos := s.index_after_('+', paren_start) + if plus_pos < 0 { + return s + } + symbol := s[paren_start + 1..plus_pos] + if symbol.len == 0 { + return s + } + return s[..paren_start + 1] + demangle_v_symbol(symbol) + s[plus_pos..] +} + fn eprint_space_padding(output string, max_len int) { padding_len := max_len - output.len if padding_len > 0 { diff --git a/vlib/builtin/backtraces_nix.c.v b/vlib/builtin/backtraces_nix.c.v index 529813aa1..514559ff5 100644 --- a/vlib/builtin/backtraces_nix.c.v +++ b/vlib/builtin/backtraces_nix.c.v @@ -31,12 +31,110 @@ fn print_backtrace_skipping_top_frames_bsd(skipframes int) bool { eprintln('C.backtrace returned less than 2 frames') return false } - C.backtrace_symbols_fd(&buffer[skipframes], nr_ptrs - skipframes, 2) + nr_actual_frames := nr_ptrs - skipframes + csymbols := C.backtrace_symbols(voidptr(&buffer[skipframes]), nr_actual_frames) + atos_lines := bsd_backtrace_resolve_atos(&buffer[skipframes], nr_actual_frames) + for i in 0 .. nr_actual_frames { + sframe := unsafe { tos2(&u8(csymbols[i])) } + mut file_line := '' + if i < atos_lines.len { + file_line = atos_lines[i] + } + // macOS format: `0 main 0x00000001047232f8 veb__run_T_main__App_main__Context + 356` + symbol_start := sframe.index('0x') or { -1 } + if symbol_start < 0 { + continue + } + rest := sframe[symbol_start..] + space_after_addr := rest.index(' ') or { -1 } + if space_after_addr < 0 { + continue + } + symbol_and_offset := rest[space_after_addr + 1..] + plus_pos := symbol_and_offset.index(' + ') or { -1 } + mut raw_symbol := symbol_and_offset + if plus_pos >= 0 { + raw_symbol = symbol_and_offset[..plus_pos] + } + // Skip C runtime frames that are not V functions: + if raw_symbol in ['main', 'start', '_main'] { + continue + } + mut demangled := '' + if plus_pos >= 0 { + demangled = demangle_v_symbol(raw_symbol) + symbol_and_offset[plus_pos..] + } else { + demangled = demangle_v_symbol(raw_symbol) + } + if file_line.len > 0 { + eprint(file_line) + eprint_space_padding(file_line, 45) + eprint(' | ') + eprintln(demangled) + } else { + eprintln(demangled) + } + } + if nr_actual_frames > 0 { + unsafe { C.free(csymbols) } + } } return true } } +// bsd_backtrace_resolve_atos uses macOS `atos` to resolve addresses to file:line info. +// Returns an array of file:line strings (one per frame). Empty string if unresolved. +@[direct_array_access] +fn bsd_backtrace_resolve_atos(buffer &voidptr, nr_frames int) []string { + $if macos { + exe_name := backtrace_current_executable_name() + if exe_name.len == 0 { + return []string{} + } + base_addr := C._dyld_get_image_header(0) + if base_addr == unsafe { nil } { + return []string{} + } + // Build single atos command with all addresses for efficiency: + mut cmd := 'atos --fullPath -o "' + exe_name + '" -l ' + ptr_str(base_addr) + for i in 0 .. nr_frames { + cmd += ' ' + ptr_str(unsafe { buffer[i] }) + } + f := C.popen(&char(cmd.str), c'r') + if f == unsafe { nil } { + return []string{} + } + buf := [4096]u8{} + mut lines := []string{cap: nr_frames} + unsafe { + bp := &buf[0] + for C.fgets(&char(bp), 4096, f) != 0 { + line := tos(bp, vstrlen(bp)).trim_chars(' \t\n\r', .trim_both) + // atos output format: `func_name (in binary) (file.v:42)` + // Extract the last parenthesized (file:line) part: + paren_pos := line.index_last_('(') + if paren_pos >= 0 { + file_part := line[paren_pos + 1..] + end_paren := file_part.index_last_(')') + if end_paren >= 0 { + file_line := file_part[..end_paren] + if file_line.contains(':') && !file_line.starts_with('in ') + && !file_line.contains('.tmp.c:') { + lines << file_line + continue + } + } + } + lines << '' + } + } + C.pclose(f) + return lines + } + return []string{} +} + fn C.tcc_backtrace(fmt &char) i32 fn backtrace_current_executable_name() string { @@ -132,7 +230,7 @@ fn print_backtrace_skipping_top_frames_linux(skipframes int) bool { eprint(' | ') eprint(addr) eprint(' | ') - eprintln(beforeaddr) + eprintln(demangle_backtrace_sym(beforeaddr)) } if nr_actual_frames > 0 { unsafe { C.free(csymbols) } diff --git a/vlib/builtin/backtraces_windows.c.v b/vlib/builtin/backtraces_windows.c.v index 69ff1600c..cf42f6f76 100644 --- a/vlib/builtin/backtraces_windows.c.v +++ b/vlib/builtin/backtraces_windows.c.v @@ -122,7 +122,7 @@ fn print_backtrace_skipping_top_frames_msvc(skipframes int) bool { // addr: lineinfo = '?? : address = 0x' + ptr_str(frame_addr) } - sfunc := unsafe { tos3(fname) } + sfunc := demangle_v_symbol(unsafe { tos3(fname) }) snframe := i64(nframe).str() eprint_space_padding(snframe, 2) eprint(': ') diff --git a/vlib/builtin/builtin_backtraces_nix.c.v b/vlib/builtin/builtin_backtraces_nix.c.v index d53660953..6499e2ba1 100644 --- a/vlib/builtin/builtin_backtraces_nix.c.v +++ b/vlib/builtin/builtin_backtraces_nix.c.v @@ -4,3 +4,9 @@ module builtin fn C.backtrace(a &voidptr, size i32) i32 fn C.backtrace_symbols(a &voidptr, size i32) &&char fn C.backtrace_symbols_fd(a &voidptr, size i32, fd i32) + +$if macos { + #include + + fn C._dyld_get_image_header(image_index u32) voidptr +} diff --git a/vlib/builtin/builtin_d_use_libbacktrace.c.v b/vlib/builtin/builtin_d_use_libbacktrace.c.v index 79c11df59..133233ae8 100644 --- a/vlib/builtin/builtin_d_use_libbacktrace.c.v +++ b/vlib/builtin/builtin_d_use_libbacktrace.c.v @@ -47,7 +47,7 @@ fn bt_print_callback(data &BacktraceOptions, pc voidptr, filename_ptr &char, lin fn_name := if fn_name_ptr == unsafe { nil } { '???' } else { - (unsafe { fn_name_ptr.vstring() }).replace('__', '.') + demangle_v_symbol(unsafe { fn_name_ptr.vstring() }) } // keep it for later // pc_64 := u64(pc) diff --git a/vlib/builtin/string_d_ownership.v b/vlib/builtin/string_d_ownership.v index f78925f25..e9493c1a5 100644 --- a/vlib/builtin/string_d_ownership.v +++ b/vlib/builtin/string_d_ownership.v @@ -3,6 +3,12 @@ // Provides .to_owned() which creates an owned string with move semantics. module builtin +// IClone marks values that provide explicit clone semantics in ownership mode. +// +// This is a marker interface. Concrete types still define their own `clone()` +// methods with concrete return types. +pub interface IClone {} + // to_owned creates an owned copy of the string. // When ownership checking is enabled, owned strings have move semantics: // assigning an owned string to another variable moves ownership, diff --git a/vlib/v/gen/c/fn.v b/vlib/v/gen/c/fn.v index 6f87bf9cc..35b93d3a0 100644 --- a/vlib/v/gen/c/fn.v +++ b/vlib/v/gen/c/fn.v @@ -3384,8 +3384,8 @@ fn (mut g Gen) unwrap_receiver_type(node ast.CallExpr) (ast.Type, &ast.TypeSymbo mut typ_sym := g.table.sym(unwrapped_rec_type) mut left_sym := g.table.sym(left_type) if left_type != 0 && left_type != g.unwrap_generic(node.receiver_type) - && left_sym.kind != .aggregate - && left_sym.has_method(node.name) && node.from_embed_types.len == 0 { + && left_sym.kind != .aggregate && left_sym.has_method(node.name) + && node.from_embed_types.len == 0 { unwrapped_rec_type = left_type typ_sym = left_sym } @@ -3522,8 +3522,7 @@ fn (mut g Gen) method_call(node ast.CallExpr) { if left_type != 0 && (left_type != g.unwrap_generic(node.receiver_type) || left_type != unwrapped_rec_type) { resolved_left_sym := g.table.sym(left_type) - if resolved_left_sym.kind != .aggregate - && (resolved_left_sym.has_method(method_name) + if resolved_left_sym.kind != .aggregate && (resolved_left_sym.has_method(method_name) || resolved_left_sym.has_method_with_generic_parent(method_name)) { unwrapped_rec_type = left_type receiver_type = left_type.derive(node.receiver_type).clear_flag(.generic) diff --git a/vlib/v2/ast/ast.v b/vlib/v2/ast/ast.v index e1d31e605..5510f97dc 100644 --- a/vlib/v2/ast/ast.v +++ b/vlib/v2/ast/ast.v @@ -925,6 +925,7 @@ pub: attributes []Attribute is_public bool is_union bool + implements []Expr embedded []Expr language Language = .v name string diff --git a/vlib/v2/ast_dump/ast_dump.v b/vlib/v2/ast_dump/ast_dump.v index 84717e566..6eb31160a 100644 --- a/vlib/v2/ast_dump/ast_dump.v +++ b/vlib/v2/ast_dump/ast_dump.v @@ -655,6 +655,11 @@ fn (mut jb JsonBuilder) write_interface_decl(stmt ast.InterfaceDecl) { jb.write_exprs(stmt.generic_params) jb.sb.write_string(',\n') + jb.write_indent() + jb.sb.write_string('"implements": ') + jb.write_exprs(stmt.implements) + jb.sb.write_string(',\n') + jb.write_indent() jb.sb.write_string('"embedded": ') jb.write_exprs(stmt.embedded) diff --git a/vlib/v2/builder/cache_headers.v b/vlib/v2/builder/cache_headers.v index 9955ed92e..805efa146 100644 --- a/vlib/v2/builder/cache_headers.v +++ b/vlib/v2/builder/cache_headers.v @@ -650,6 +650,7 @@ fn (b &Builder) build_module_header_ast(source_files []ast.File, module_name str decl_stmts << ast.Stmt(ast.StructDecl{ is_public: stmt.is_public is_union: stmt.is_union + implements: stmt.implements embedded: stmt.embedded language: stmt.language name: stmt.name diff --git a/vlib/v2/gen/v/gen.v b/vlib/v2/gen/v/gen.v index cd5eb2ec8..cdc6762df 100644 --- a/vlib/v2/gen/v/gen.v +++ b/vlib/v2/gen/v/gen.v @@ -945,6 +945,15 @@ fn (mut g Gen) struct_decl(stmt ast.StructDecl) { if stmt.generic_params.len > 0 { g.generic_list(stmt.generic_params) } + if stmt.implements.len > 0 { + g.write(' implements ') + for i, expr in stmt.implements { + if i > 0 { + g.write(', ') + } + g.expr(expr) + } + } g.struct_decl_fields(stmt.embedded, stmt.fields) } diff --git a/vlib/v2/parser/parser.v b/vlib/v2/parser/parser.v index 52de98a3d..78c488630 100644 --- a/vlib/v2/parser/parser.v +++ b/vlib/v2/parser/parser.v @@ -2457,7 +2457,19 @@ fn (mut p Parser) struct_decl(is_public bool, attributes []ast.Attribute) ast.St language := p.decl_language() name := p.expect_name() // p.log('ast.StructDecl: ${name}') - generic_params := if p.tok == .lsbr { p.generic_list() } else { []ast.Expr{} } + mut generic_params := []ast.Expr{} + mut impl_types := []ast.Expr{} + if p.tok == .lsbr { + generic_params = p.generic_list() + } + if p.tok == .name && p.lit == 'implements' { + p.next() + impl_types << p.expect_type() + for p.tok == .comma { + p.next() + impl_types << p.expect_type() + } + } // probably C struct decl with no body or {} if p.tok != .lcbr { if language == .v { @@ -2466,6 +2478,7 @@ fn (mut p Parser) struct_decl(is_public bool, attributes []ast.Attribute) ast.St return ast.StructDecl{ is_public: is_public is_union: is_union + implements: impl_types language: language name: name generic_params: generic_params @@ -2477,6 +2490,7 @@ fn (mut p Parser) struct_decl(is_public bool, attributes []ast.Attribute) ast.St attributes: attributes is_public: is_public is_union: is_union + implements: impl_types embedded: embedded language: language name: name diff --git a/vlib/v2/transformer/fn.v b/vlib/v2/transformer/fn.v index 3b58a8dbb..4056798a9 100644 --- a/vlib/v2/transformer/fn.v +++ b/vlib/v2/transformer/fn.v @@ -1352,6 +1352,11 @@ fn (mut t Transformer) transform_call_expr(expr ast.CallExpr) ast.Expr { } } } + if sel.rhs.name == 'clone' && expr.args.len == 0 { + if recv_type := t.get_expr_type(sel.lhs) { + _ = t.auto_clone_fn_name_for_type(recv_type) + } + } // insert(i, arr) → insert_many(i, arr.data, arr.len) if resolved.ends_with('__insert') && expr.args.len == 2 { if arg_type := t.get_expr_type(expr.args[1]) { @@ -1899,6 +1904,11 @@ fn (t &Transformer) resolve_method_call_name(receiver ast.Expr, method_name stri } } } + if method_name == 'clone' { + if generated := t.clone_fn_name_for_type(recv_type) { + return generated + } + } return none } diff --git a/vlib/v2/transformer/transformer.v b/vlib/v2/transformer/transformer.v index 477c02b98..6edd0a099 100644 --- a/vlib/v2/transformer/transformer.v +++ b/vlib/v2/transformer/transformer.v @@ -37,6 +37,8 @@ mut: synth_pos_counter int = -1 // Track needed auto-generated str functions (type_name -> elem_type for arrays) needed_str_fns map[string]string + // Track needed auto-generated clone functions (fn_name -> struct_name) + needed_clone_fns map[string]string // Track needed auto-generated array helper functions needed_array_contains_fns map[string]ArrayMethodInfo needed_array_index_fns map[string]ArrayMethodInfo @@ -186,6 +188,7 @@ pub fn Transformer.new_with_pref(files []ast.File, env &types.Environment, p &pr pref: unsafe { p } env: unsafe { env } needed_str_fns: map[string]string{} + needed_clone_fns: map[string]string{} needed_array_contains_fns: map[string]ArrayMethodInfo{} needed_array_index_fns: map[string]ArrayMethodInfo{} needed_array_last_index_fns: map[string]ArrayMethodInfo{} @@ -218,6 +221,7 @@ pub fn (t &Transformer) new_worker_clone(worker_idx int) &Transformer { cached_fn_scopes: t.cached_fn_scopes synth_pos_counter: -(worker_idx * 100_000) needed_str_fns: map[string]string{} + needed_clone_fns: map[string]string{} needed_array_contains_fns: map[string]ArrayMethodInfo{} needed_array_index_fns: map[string]ArrayMethodInfo{} needed_array_last_index_fns: map[string]ArrayMethodInfo{} @@ -233,6 +237,9 @@ pub fn (mut t Transformer) merge_worker(w &Transformer) { for k, v in w.needed_str_fns { t.needed_str_fns[k] = v } + for k, v in w.needed_clone_fns { + t.needed_clone_fns[k] = v + } for k, v in w.needed_array_contains_fns { t.needed_array_contains_fns[k] = v } @@ -783,6 +790,9 @@ pub fn (mut t Transformer) post_pass(mut result []ast.File) { if t.needed_str_fns.len > 0 { generated_fns << t.generate_str_functions() } + if t.needed_clone_fns.len > 0 { + generated_fns << t.generate_clone_functions() + } if t.needed_sort_fns.len > 0 { generated_fns << t.generate_sort_comparator_functions() } @@ -8763,6 +8773,177 @@ fn generated_fn_module(fn_name string, files []ast.File) string { return '' } +fn clone_generated_fn_scope_module(struct_name string) string { + if !struct_name.contains('__') { + return 'main' + } + return struct_name.all_before('__') +} + +fn (mut t Transformer) clone_value_expr(expr ast.Expr, typ types.Type) ast.Expr { + resolved := types.resolve_alias(typ) + match resolved { + types.String { + return ast.Expr(ast.CallExpr{ + lhs: ast.Ident{ + name: 'string__clone' + } + args: [expr] + }) + } + types.Array { + depth := t.get_array_nesting_depth(resolved) + if depth > 1 { + return ast.Expr(ast.CallExpr{ + lhs: ast.Ident{ + name: 'array__clone_to_depth' + } + args: [ + expr, + ast.Expr(ast.BasicLiteral{ + kind: .number + value: '${depth - 1}' + }), + ] + }) + } + return ast.Expr(ast.CallExpr{ + lhs: ast.Ident{ + name: 'array__clone' + } + args: [expr] + }) + } + types.Map { + return ast.Expr(ast.CallExpr{ + lhs: ast.Ident{ + name: 'map__clone' + } + args: [expr] + }) + } + types.Struct { + if clone_fn_name := t.auto_clone_fn_name_for_type(resolved) { + return ast.Expr(ast.CallExpr{ + lhs: ast.Ident{ + name: clone_fn_name + } + args: [expr] + }) + } + } + else {} + } + return expr +} + +fn (mut t Transformer) generate_struct_clone_fn(fn_name string, struct_name string, struct_type types.Struct) ast.Stmt { + param_s := ast.Parameter{ + name: 's' + typ: ast.Ident{ + name: struct_name + } + } + mut fields := []ast.FieldInit{cap: struct_type.embedded.len + struct_type.fields.len} + for embedded in struct_type.embedded { + embedded_name := if embedded.name.contains('__') { + embedded.name.all_after_last('__') + } else { + embedded.name + } + if embedded_name == '' { + continue + } + field_selector := t.synth_selector(ast.Expr(ast.Ident{ + name: 's' + }), embedded_name, types.Type(embedded)) + fields << ast.FieldInit{ + name: embedded_name + value: t.clone_value_expr(field_selector, types.Type(embedded)) + } + } + for field in struct_type.fields { + field_selector := t.synth_selector_from_struct(ast.Expr(ast.Ident{ + name: 's' + }), field.name, struct_name) + fields << ast.FieldInit{ + name: field.name + value: t.clone_value_expr(field_selector, field.typ) + } + } + t.register_generated_fn_scope(fn_name, clone_generated_fn_scope_module(struct_name), + [param_s]) + return ast.Stmt(ast.FnDecl{ + name: fn_name + typ: ast.FnType{ + params: [param_s] + return_type: ast.Ident{ + name: struct_name + } + } + stmts: [ + ast.Stmt(ast.ReturnStmt{ + exprs: [ + ast.Expr(ast.InitExpr{ + typ: ast.Ident{ + name: struct_name + } + fields: fields + }), + ] + }), + ] + }) +} + +fn (mut t Transformer) generate_clone_functions() []ast.Stmt { + mut result := []ast.Stmt{cap: t.needed_clone_fns.len} + mut generated := map[string]bool{} + for { + mut found_new := false + for fn_name, struct_name in t.needed_clone_fns { + if fn_name in generated { + continue + } + generated[fn_name] = true + found_new = true + if typ := t.lookup_struct_type_any_module(struct_name) { + result << t.generate_struct_clone_fn(fn_name, struct_name, typ) + continue + } + result << ast.Stmt(ast.FnDecl{ + name: fn_name + typ: ast.FnType{ + params: [ + ast.Parameter{ + name: 's' + typ: ast.Ident{ + name: struct_name + } + }, + ] + return_type: ast.Ident{ + name: struct_name + } + } + stmts: [ + ast.Stmt(ast.ReturnStmt{ + exprs: [ + ast.Expr(ast.Ident{ + name: 's' + }), + ] + }), + ] + }) + } + if !found_new { + break + } + } + return result +} + fn (mut t Transformer) generate_str_functions() []ast.Stmt { mut result := []ast.Stmt{cap: t.needed_str_fns.len} // Use worklist to handle recursive str function registration diff --git a/vlib/v2/transformer/transformer_test.v b/vlib/v2/transformer/transformer_test.v index 08684f56c..d8a9e7b08 100644 --- a/vlib/v2/transformer/transformer_test.v +++ b/vlib/v2/transformer/transformer_test.v @@ -5,6 +5,7 @@ module transformer import os import v2.ast +import v2.parser import v2.pref as vpref import v2.token import v2.types @@ -15,6 +16,7 @@ fn create_test_transformer() &Transformer { return &Transformer{ pref: &vpref.Preferences{} env: unsafe { env } + needed_clone_fns: map[string]string{} needed_array_contains_fns: map[string]ArrayMethodInfo{} needed_array_index_fns: map[string]ArrayMethodInfo{} needed_array_last_index_fns: map[string]ArrayMethodInfo{} @@ -33,12 +35,33 @@ fn create_transformer_with_vars(vars map[string]types.Type) &Transformer { pref: &vpref.Preferences{} env: unsafe { env } scope: scope + needed_clone_fns: map[string]string{} needed_array_contains_fns: map[string]ArrayMethodInfo{} needed_array_index_fns: map[string]ArrayMethodInfo{} needed_array_last_index_fns: map[string]ArrayMethodInfo{} } } +fn transform_code_for_test(code string) []ast.File { + tmp_file := '/tmp/v2_transformer_test_${os.getpid()}.v' + os.write_file(tmp_file, code) or { panic('failed to write temp file') } + defer { + os.rm(tmp_file) or {} + } + prefs := &vpref.Preferences{ + backend: .cleanc + no_parallel: true + } + mut file_set := token.FileSet.new() + mut par := parser.Parser.new(prefs) + files := par.parse_files([tmp_file], mut file_set) + env := types.Environment.new() + mut checker := types.Checker.new(prefs, file_set, env) + checker.check_files(files) + mut transformer := Transformer.new_with_pref(files, env, prefs) + return transformer.transform_files(files) +} + // string_type returns the builtin v2 string type. fn string_type() types.Type { return types.string_ @@ -205,6 +228,67 @@ fn test_transform_ident_vmodroot_empty_root() { assert lit.value == "''" } +fn test_transform_autogenerates_clone_helpers_for_iclone_structs() { + files := transform_code_for_test(' +interface IClone {} + +struct Inner implements IClone { + name string +} + +struct Outer implements IClone { + inner Inner + nums []int +} + +fn use_clone(value Outer) Outer { + return value.clone() +} +') + assert files.len == 1 + file := files[0] + mut saw_inner_clone := false + mut saw_outer_clone := false + mut saw_lowered_call := false + for stmt in file.stmts { + if stmt is ast.FnDecl && stmt.name == 'use_clone' { + assert stmt.stmts.len == 1 + ret := stmt.stmts[0] as ast.ReturnStmt + assert ret.exprs.len == 1 + assert ret.exprs[0] is ast.CallExpr + call := ret.exprs[0] as ast.CallExpr + assert call.lhs is ast.Ident + assert (call.lhs as ast.Ident).name == 'Outer__clone' + saw_lowered_call = true + } + if stmt is ast.FnDecl && stmt.name == 'Inner__clone' { + saw_inner_clone = true + } + if stmt is ast.FnDecl && stmt.name == 'Outer__clone' { + saw_outer_clone = true + assert stmt.stmts.len == 1 + ret := stmt.stmts[0] as ast.ReturnStmt + assert ret.exprs.len == 1 + assert ret.exprs[0] is ast.InitExpr + init := ret.exprs[0] as ast.InitExpr + assert init.fields.len == 2 + assert init.fields[0].name == 'inner' + assert init.fields[0].value is ast.CallExpr + inner_call := init.fields[0].value as ast.CallExpr + assert inner_call.lhs is ast.Ident + assert (inner_call.lhs as ast.Ident).name == 'Inner__clone' + assert init.fields[1].name == 'nums' + assert init.fields[1].value is ast.CallExpr + nums_call := init.fields[1].value as ast.CallExpr + assert nums_call.lhs is ast.Ident + assert (nums_call.lhs as ast.Ident).name == 'array__clone' + } + } + assert saw_lowered_call + assert saw_inner_clone + assert saw_outer_clone +} + fn test_array_comparison_eq() { // Set up variable types so get_array_type_str can detect them mut t := create_transformer_with_vars({ diff --git a/vlib/v2/transformer/types.v b/vlib/v2/transformer/types.v index 46fc5e5a3..d91381f3d 100644 --- a/vlib/v2/transformer/types.v +++ b/vlib/v2/transformer/types.v @@ -323,6 +323,91 @@ fn (t &Transformer) lookup_type(name string) ?types.Type { return none } +fn (t &Transformer) lookup_struct_type_any_module(name string) ?types.Struct { + if typ := t.lookup_type(name) { + if typ is types.Struct { + return typ + } + } + for _, scope in t.cached_scopes { + if obj := scope.objects[name] { + if obj is types.Type { + if obj is types.Struct { + return obj + } + } + } + } + return none +} + +fn (t &Transformer) struct_implements_name(st types.Struct, target string) bool { + for impl_name in st.implements { + if impl_name == target || impl_name.all_after_last('__') == target { + return true + } + } + if st.name != '' && st.implements.len == 0 { + if live_type := t.lookup_struct_type_any_module(st.name) { + if live_type.name != st.name { + return t.struct_implements_name(live_type, target) + } + for impl_name in live_type.implements { + if impl_name == target || impl_name.all_after_last('__') == target { + return true + } + } + } + } + return false +} + +fn (mut t Transformer) register_needed_clone_struct(st types.Struct) string { + fn_name := '${t.type_to_c_name(types.Type(st))}__clone' + t.needed_clone_fns[fn_name] = st.name + return fn_name +} + +fn (t &Transformer) clone_fn_name_for_type(typ types.Type) ?string { + mut base := typ + for base is types.Pointer { + base = (base as types.Pointer).base_type + } + base = types.resolve_alias(base) + if base is types.Struct { + st := base as types.Struct + if !t.struct_implements_name(st, 'IClone') { + return none + } + fn_name := '${t.type_to_c_name(types.Type(st))}__clone' + short_name := if st.name.contains('__') { st.name.all_after_last('__') } else { st.name } + if t.lookup_method_cached(st.name, 'clone') != none + || (short_name != st.name && t.lookup_method_cached(short_name, 'clone') != none) { + return fn_name + } + return fn_name + } + return none +} + +fn (mut t Transformer) auto_clone_fn_name_for_type(typ types.Type) ?string { + fn_name := t.clone_fn_name_for_type(typ) or { return none } + mut base := typ + for base is types.Pointer { + base = (base as types.Pointer).base_type + } + base = types.resolve_alias(base) + if base is types.Struct { + st := base as types.Struct + short_name := if st.name.contains('__') { st.name.all_after_last('__') } else { st.name } + if t.lookup_method_cached(st.name, 'clone') == none + && (short_name == st.name || t.lookup_method_cached(short_name, 'clone') == none) { + t.needed_clone_fns[fn_name] = st.name + } + } + return fn_name +} + // is_flag_enum checks if a type name is a flag enum fn (t &Transformer) is_flag_enum(type_name string) bool { typ := t.lookup_type(type_name) or { return false } diff --git a/vlib/v2/types/checker.v b/vlib/v2/types/checker.v index 82a4adb15..3a31fb6a2 100644 --- a/vlib/v2/types/checker.v +++ b/vlib/v2/types/checker.v @@ -1242,7 +1242,7 @@ fn (mut c Checker) infix_expr(expr ast.InfixExpr) Type { $if ownership ? { lhs_base := lhs_type.base_type() if expr.op == .left_shift && (lhs_type is Array || lhs_base is Array) { - c.ownership_consume_expr(expr.rhs, expr.rhs.pos(), "array append") + c.ownership_consume_expr(expr.rhs, expr.rhs.pos(), 'array append') } } c.expected_type = expected_type @@ -1297,7 +1297,7 @@ fn (mut c Checker) init_expr(expr ast.InitExpr) Type { } c.expr(field.value) $if ownership ? { - c.ownership_consume_expr(field.value, field.value.pos(), "struct field") + c.ownership_consume_expr(field.value, field.value.pos(), 'struct field') } c.expected_type = expected_type_prev } @@ -1307,7 +1307,7 @@ fn (mut c Checker) init_expr(expr ast.InitExpr) Type { if field.value !is ast.EmptyExpr { c.expr(field.value) $if ownership ? { - c.ownership_consume_expr(field.value, field.value.pos(), "struct field") + c.ownership_consume_expr(field.value, field.value.pos(), 'struct field') } } } @@ -1384,8 +1384,8 @@ fn (mut c Checker) map_init_expr(expr ast.MapInitExpr) Type { map_key_type = c.expr(expr.keys[0]).typed_default() map_value_type = c.expr(expr.vals[0]).typed_default() $if ownership ? { - c.ownership_consume_expr(expr.keys[0], expr.keys[0].pos(), "map key") - c.ownership_consume_expr(expr.vals[0], expr.vals[0].pos(), "map value") + c.ownership_consume_expr(expr.keys[0], expr.keys[0].pos(), 'map key') + c.ownership_consume_expr(expr.vals[0], expr.vals[0].pos(), 'map value') } has_map_type = true inferred_from_first_entry = true @@ -1401,8 +1401,8 @@ fn (mut c Checker) map_init_expr(expr ast.MapInitExpr) Type { c.expected_type = to_optional_type(map_value_type) val_type := c.expr(val_expr).typed_default() $if ownership ? { - c.ownership_consume_expr(key_expr, key_expr.pos(), "map key") - c.ownership_consume_expr(val_expr, val_expr.pos(), "map value") + c.ownership_consume_expr(key_expr, key_expr.pos(), 'map key') + c.ownership_consume_expr(val_expr, val_expr.pos(), 'map value') } if !c.check_types(map_key_type, key_type) { c.error_with_pos('invalid map key: expecting ${map_key_type.name()}, got ${key_type.name()}', @@ -1540,7 +1540,8 @@ fn (mut c Checker) expr_impl(expr ast.Expr) Type { // TODO: check all exprs first_elem_type := c.expr(expr.exprs.first()) $if ownership ? { - c.ownership_consume_expr(expr.exprs.first(), expr.exprs.first().pos(), "array element") + c.ownership_consume_expr(expr.exprs.first(), expr.exprs.first().pos(), + 'array element') } // NOTE: why did I have this shortcut here? // if expr.exprs.len == 1 { @@ -1569,7 +1570,7 @@ fn (mut c Checker) expr_impl(expr ast.Expr) Type { } mut elem_type := c.expr(elem_expr) $if ownership ? { - c.ownership_consume_expr(elem_expr, elem_expr.pos(), "array element") + c.ownership_consume_expr(elem_expr, elem_expr.pos(), 'array element') } // TODO: best way to handle this? if elem_type.is_number_literal() && first_elem_type.is_number() { @@ -2405,6 +2406,19 @@ fn (mut c Checker) process_pending_struct_decls() { c.scope.insert(gp_name, Type(NamedType(gp_name))) } } + mut implements_names := []string{} + for impl_expr in pending.decl.implements { + impl_type := c.type_expr(impl_expr) + impl_name := impl_type.name() + if impl_name != '' && impl_name != 'void' { + implements_names << impl_name + continue + } + fallback_name := c.type_ref_name(impl_expr) + if fallback_name != '' { + implements_names << fallback_name + } + } mut fields := []Field{} for field in pending.decl.fields { field_typ := c.type_expr(field.typ) @@ -2464,6 +2478,7 @@ fn (mut c Checker) process_pending_struct_decls() { update_scope.objects[pending.decl.name] = object_from_type(Type(Struct{ name: sd.name generic_params: sd.generic_params + implements: implements_names fields: fields embedded: embedded is_soa: is_soa @@ -2641,7 +2656,7 @@ fn (mut c Checker) check_struct_field_defaults(files []ast.File) { c.expected_type = to_optional_type(field_typ) c.expr(field.value) $if ownership ? { - c.ownership_consume_expr(field.value, field.value.pos(), "struct field") + c.ownership_consume_expr(field.value, field.value.pos(), 'struct field') } c.expected_type = prev_expected } @@ -2846,7 +2861,7 @@ fn (mut c Checker) assign_stmt(stmt ast.AssignStmt, unwrap_optional bool) { } else if stmt.op in [.left_shift, .left_shift_assign] { lhs_base := lhs_type.base_type() if lhs_type is Array || lhs_base is Array { - c.ownership_consume_expr(rx, rx.pos(), "array append") + c.ownership_consume_expr(rx, rx.pos(), 'array append') } } } @@ -3443,9 +3458,10 @@ fn (mut c Checker) instantiate_generic_struct(base Struct, args []ast.Expr) Type } } return Struct{ - name: actual_base.name - fields: fields - embedded: embedded + name: actual_base.name + implements: actual_base.implements + fields: fields + embedded: embedded } } @@ -3460,6 +3476,7 @@ fn (mut c Checker) substitute_struct_generic_type_with_stack(st Struct, generic_ return Struct{ name: st.name generic_params: st.generic_params + implements: st.implements is_soa: st.is_soa } } @@ -3484,6 +3501,7 @@ fn (mut c Checker) substitute_struct_generic_type_with_stack(st Struct, generic_ return Struct{ name: st.name generic_params: st.generic_params + implements: st.implements fields: fields embedded: embedded is_soa: st.is_soa @@ -4586,6 +4604,29 @@ fn (mut c Checker) resolve_interface_fields(iface Interface) []Field { return iface.fields } +fn (mut c Checker) struct_implements_name(st Struct, target string) bool { + for impl_name in st.implements { + if impl_name == target || impl_name.all_after_last('__') == target { + return true + } + } + if st.name != '' && st.implements.len == 0 { + if obj := c.lookup_type_by_name(st.name) { + if obj is Struct && obj.name != st.name { + return c.struct_implements_name(obj, target) + } + if obj is Struct { + for impl_name in obj.implements { + if impl_name == target || impl_name.all_after_last('__') == target { + return true + } + } + } + } + } + return false +} + fn (mut c Checker) find_field_or_method(t Type, raw_name string) !Type { // Strip @ prefix used to escape V keywords in field/method names (e.g., @type → type) name := if raw_name.len > 0 && raw_name[0] == `@` { raw_name[1..] } else { raw_name } @@ -4863,6 +4904,9 @@ fn (mut c Checker) find_field_or_method(t Type, raw_name string) !Type { else {} } } + if name == 'clone' && c.struct_implements_name(t, 'IClone') { + return fn_with_return_type(empty_fn_type(), Type(t)) + } // If struct has no fields (stale copy from self-referential generic instantiation), // look up the generic template from scope and use its field types. if t.fields.len == 0 && t.embedded.len == 0 && t.name != '' { diff --git a/vlib/v2/types/checker_ownership.v b/vlib/v2/types/checker_ownership.v index d77308f37..7d1e6608e 100644 --- a/vlib/v2/types/checker_ownership.v +++ b/vlib/v2/types/checker_ownership.v @@ -9,11 +9,11 @@ import v2.token // MovedVar tracks information about a variable that has been moved. pub struct MovedVar { pub: - moved_to string // name of the variable or function parameter it was moved to - move_pos token.Pos // position where the move occurred - is_fn_call bool // true if moved via function call argument + moved_to string // name of the variable or function parameter it was moved to + move_pos token.Pos // position where the move occurred + is_fn_call bool // true if moved via function call argument suggest_clone bool = true // show clone suggestion in diagnostics - fn_name string // function name (only when is_fn_call is true) + fn_name string // function name (only when is_fn_call is true) } // BorrowInfo tracks an active borrow of a variable. diff --git a/vlib/v2/types/checker_test.v b/vlib/v2/types/checker_test.v index cbedef5b6..28125f594 100644 --- a/vlib/v2/types/checker_test.v +++ b/vlib/v2/types/checker_test.v @@ -94,6 +94,22 @@ fn test_basic_literal_string() { assert has_type(env, 'string'), 'string literal should have string type' } +fn test_iclone_struct_clone_returns_self_type() { + env := check_code(' +interface IClone {} + +struct Foo implements IClone { + x int +} + +fn main() { + f := Foo{x: 1} + y := f.clone().x +} +') + assert has_type(env, 'int'), 'f.clone().x should type-check as int' +} + fn test_or_expr_accepts_int_literal_fallback() { env := check_code('fn may_fail() !int { return 1 } fn main() { x := may_fail() or { 0 } }') diff --git a/vlib/v2/types/types.v b/vlib/v2/types/types.v index 414a5afda..bfe2267ce 100644 --- a/vlib/v2/types/types.v +++ b/vlib/v2/types/types.v @@ -231,6 +231,7 @@ pub struct Struct { pub: name string generic_params []string + implements []string pub mut: embedded []Struct // embedded []Type -- 2.39.5