From de87ae7c634d15f3cd737bde139ed9ff2aafc105 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 11 Mar 2026 16:42:21 +0300 Subject: [PATCH] cgen: memory leak in many of the examples in V (fixes #24738) --- vlib/v/gen/c/autofree.v | 25 ++++++++----- vlib/v/gen/c/cgen.v | 12 +++--- vlib/v/gen/c/fn.v | 37 +++++++++++++++---- vlib/v/gen/c/testdata/print_boehm_leak.out | 1 + vlib/v/gen/c/testdata/print_boehm_leak.vv | 8 ++++ .../c/testdata/scope_cleanup_boehm_leak.out | 1 + .../c/testdata/scope_cleanup_boehm_leak.vv | 26 +++++++++++++ vlib/v/markused/walker.v | 15 +++++++- 8 files changed, 100 insertions(+), 25 deletions(-) create mode 100644 vlib/v/gen/c/testdata/print_boehm_leak.out create mode 100644 vlib/v/gen/c/testdata/print_boehm_leak.vv create mode 100644 vlib/v/gen/c/testdata/scope_cleanup_boehm_leak.out create mode 100644 vlib/v/gen/c/testdata/scope_cleanup_boehm_leak.vv diff --git a/vlib/v/gen/c/autofree.v b/vlib/v/gen/c/autofree.v index d4e55189d..3e7a91e96 100644 --- a/vlib/v/gen/c/autofree.v +++ b/vlib/v/gen/c/autofree.v @@ -6,8 +6,12 @@ module c import strings import v.ast +fn (g &Gen) needs_scope_cleanup() bool { + return g.is_autofree || g.pref.gc_mode == .boehm_leak +} + fn (mut g Gen) autofree_scope_vars(pos int, line_nr int, free_parent_scopes bool) { - if !g.is_autofree { + if !g.needs_scope_cleanup() { return } // g.writeln('// afsv pos=${pos} line_nr=${line_nr} freeparent_scopes=${free_parent_scopes}') @@ -15,7 +19,7 @@ fn (mut g Gen) autofree_scope_vars(pos int, line_nr int, free_parent_scopes bool } fn (mut g Gen) autofree_scope_vars_stop(pos int, line_nr int, free_parent_scopes bool, stop_pos int) { - if !g.is_autofree { + if !g.needs_scope_cleanup() { return } if g.is_builtin_mod { @@ -148,13 +152,16 @@ fn (mut g Gen) autofree_variable(v ast.Var) { // eprintln(' > var name: ${v.name:-20s} | is_arg: ${v.is_arg.str():6} | var type: ${int(v.typ):8} | type_name: ${sym.name:-33s}') } // } - mut free_fn := g.styp(v.typ.set_nr_muls(0).clear_option_and_result()) + '_free' + base_typ := v.typ.set_nr_muls(0).clear_option_and_result() + mut free_fn := g.styp(base_typ) + '_free' if sym.kind == .array { - if sym.has_method('free') { - g.autofree_var_call(free_fn, v) - return - } - g.autofree_var_call('builtin__array_free', v) + free_fn = g.get_free_method(base_typ) + g.autofree_var_call(free_fn, v) + return + } + if sym.kind == .map { + free_fn = g.get_free_method(base_typ) + g.autofree_var_call(free_fn, v) return } if sym.kind == .string { @@ -212,7 +219,7 @@ fn (mut g Gen) autofree_var_call(free_fn_name string, v ast.Var) { if g.is_builtin_mod { return } - if !g.is_autofree { + if !g.needs_scope_cleanup() { return } // if v.is_autofree_tmp && !g.doing_autofree_tmp { diff --git a/vlib/v/gen/c/cgen.v b/vlib/v/gen/c/cgen.v index 0d9f0ec0d..ae4cf0b2c 100644 --- a/vlib/v/gen/c/cgen.v +++ b/vlib/v/gen/c/cgen.v @@ -2467,7 +2467,7 @@ fn (mut g Gen) stmts_with_tmp_var(stmts []ast.Stmt, tmp_var string) bool { if g.inside_ternary > 0 { g.write2('', ')') } - if g.is_autofree && !g.inside_vweb_tmpl && stmts.len > 0 && !last_stmt_was_return { + if g.needs_scope_cleanup() && !g.inside_vweb_tmpl && stmts.len > 0 && !last_stmt_was_return { // use the first stmt to get the scope stmt := stmts[0] if stmt !is ast.FnDecl && g.inside_ternary == 0 { @@ -6635,7 +6635,7 @@ fn (mut g Gen) branch_stmt(node ast.BranchStmt) { } g.write_defer_stmts(node.scope, false, node.pos) // continue or break - if g.is_autofree && !g.is_builtin_mod { + if g.needs_scope_cleanup() && !g.is_builtin_mod { g.trace_autofree('// free before continue/break') g.autofree_scope_vars_stop(node.pos.pos - 1, node.pos.line_nr, true, g.branch_parent_pos) } @@ -6689,13 +6689,13 @@ fn (mut g Gen) return_stmt(node ast.Return) { g.write_defer_stmts_when_needed(node.scope, true, node.pos) if fn_return_is_option || fn_return_is_result { styp := g.styp(fn_ret_type) - if g.is_autofree { + if g.needs_scope_cleanup() { g.trace_autofree('// free before return (no values returned)') g.autofree_scope_vars(node.pos.pos, node.pos.line_nr, false) } g.writeln('return (${styp}){0};') } else { - if g.is_autofree { + if g.needs_scope_cleanup() { g.trace_autofree('// free before return (no values returned)') g.autofree_scope_vars(node.pos.pos - 1, node.pos.line_nr, true) } @@ -6958,7 +6958,7 @@ fn (mut g Gen) return_stmt(node ast.Return) { g.expr_with_tmp_var(expr0, type0, fn_ret_type, tmpvar, false) g.writeln('') g.write_defer_stmts_when_needed(node.scope, true, node.pos) - if g.is_autofree { + if g.needs_scope_cleanup() { g.detect_used_var_on_return(expr0) } g.autofree_scope_vars(node.pos.pos - 1, node.pos.line_nr, true) @@ -6968,7 +6968,7 @@ fn (mut g Gen) return_stmt(node ast.Return) { // autofree before `return` // set free_parent_scopes to true, since all variables defined in parent // scopes need to be freed before the return - if g.is_autofree { + if g.needs_scope_cleanup() { g.detect_used_var_on_return(expr0) if !use_tmp_var && !g.is_builtin_mod { use_tmp_var = expr0 is ast.CallExpr diff --git a/vlib/v/gen/c/fn.v b/vlib/v/gen/c/fn.v index 565700123..cc2965aa1 100644 --- a/vlib/v/gen/c/fn.v +++ b/vlib/v/gen/c/fn.v @@ -2270,19 +2270,31 @@ fn (mut g Gen) fn_call(node ast.CallExpr) { if typ != ast.string_type || g.comptime.comptime_for_method != unsafe { nil } { expr := node.args[0].expr typ_sym := g.table.sym(typ) + needs_tmp_string := !typ.has_option_or_result() + && (g.is_autofree || g.pref.gc_mode == .boehm_leak) if typ_sym.kind == .interface && (typ_sym.info as ast.Interface).defines_method('str') { - g.write('builtin__${c_fn_name(print_method)}(') rec_type_name := util.no_dots(g.cc_type(typ, false)) - g.write('${c_name(rec_type_name)}_name_table[') - g.expr(expr) dot := if typ.is_ptr() { '->' } else { '.' } - g.write('${dot}_typ]._method_str(') - g.expr(expr) - g.write('${dot}_object') - g.writeln('));') + if needs_tmp_string { + tmp := g.new_tmp_var() + g.write('string ${tmp} = ${c_name(rec_type_name)}_name_table[') + g.expr(expr) + g.write('${dot}_typ]._method_str(') + g.expr(expr) + g.write('${dot}_object') + g.writeln('); builtin__${c_fn_name(print_method)}(${tmp}); builtin__string_free(&${tmp});') + } else { + g.write('builtin__${c_fn_name(print_method)}(') + g.write('${c_name(rec_type_name)}_name_table[') + g.expr(expr) + g.write('${dot}_typ]._method_str(') + g.expr(expr) + g.write('${dot}_object') + g.writeln('));') + } return } - if g.is_autofree && !typ.has_option_or_result() { + if needs_tmp_string { // Create a temporary variable so that the value can be freed tmp := g.new_tmp_var() g.write('string ${tmp} = ') @@ -2324,6 +2336,15 @@ fn (mut g Gen) fn_call(node ast.CallExpr) { } } if !print_auto_str { + if is_print && g.pref.gc_mode == .boehm_leak && node.args[0].typ == ast.string_type + && node.args[0].expr !in [ast.Ident, ast.StringLiteral, ast.SelectorExpr, ast.ComptimeSelector] { + tmp := g.new_tmp_var() + tmp_init := g.autofree_tmp_arg_init_stmt('string ${tmp} = ', node.args[0].expr) + g.writeln(tmp_init) + g.writeln('builtin__${c_fn_name(print_method)}(${tmp});') + g.writeln('builtin__string_free(&${tmp});') + return + } if is_print && node.args[0].expr !is ast.CallExpr { // only need for `println(err)` // not need for `println(err.msg())` diff --git a/vlib/v/gen/c/testdata/print_boehm_leak.out b/vlib/v/gen/c/testdata/print_boehm_leak.out new file mode 100644 index 000000000..a92d281ab --- /dev/null +++ b/vlib/v/gen/c/testdata/print_boehm_leak.out @@ -0,0 +1 @@ +0 1 2 123 diff --git a/vlib/v/gen/c/testdata/print_boehm_leak.vv b/vlib/v/gen/c/testdata/print_boehm_leak.vv new file mode 100644 index 000000000..6a5b87c8c --- /dev/null +++ b/vlib/v/gen/c/testdata/print_boehm_leak.vv @@ -0,0 +1,8 @@ +// vtest vflags: -gc boehm_leak +fn main() { + for i in 0 .. 3 { + print('${i} ') + } + println(123) + gc_check_leaks() +} diff --git a/vlib/v/gen/c/testdata/scope_cleanup_boehm_leak.out b/vlib/v/gen/c/testdata/scope_cleanup_boehm_leak.out new file mode 100644 index 000000000..45a4fb75d --- /dev/null +++ b/vlib/v/gen/c/testdata/scope_cleanup_boehm_leak.out @@ -0,0 +1 @@ +8 diff --git a/vlib/v/gen/c/testdata/scope_cleanup_boehm_leak.vv b/vlib/v/gen/c/testdata/scope_cleanup_boehm_leak.vv new file mode 100644 index 000000000..8a4b48bdd --- /dev/null +++ b/vlib/v/gen/c/testdata/scope_cleanup_boehm_leak.vv @@ -0,0 +1,26 @@ +// vtest vflags: -gc boehm_leak + +fn make_grid() [][]int { + grid := [ + [1, 2], + [3, 4], + ] + return grid +} + +fn make_label() string { + label := 'x'.repeat(3) + return label +} + +fn main() { + { + grid := make_grid() + label := make_label() + m := { + 'x': 1 + } + println(grid.len + grid[0].len + label.len + m.len) + } + gc_check_leaks() +} diff --git a/vlib/v/markused/walker.v b/vlib/v/markused/walker.v index 3415ff208..85863deac 100644 --- a/vlib/v/markused/walker.v +++ b/vlib/v/markused/walker.v @@ -463,9 +463,15 @@ fn (mut w Walker) expr(node_ ast.Expr) { if sym.info is ast.Map { w.mark_by_type(w.table.find_or_register_array(sym.info.key_type)) } - } else if !node.is_method && node.args.len == 1 && node.args[0].typ != ast.string_type + } else if !node.is_method && node.args.len == 1 && node.name in ['println', 'print', 'eprint', 'eprintln'] { - w.uses_str[node.args[0].typ] = true + if node.args[0].typ != ast.string_type { + w.uses_str[node.args[0].typ] = true + } + if w.pref.gc_mode == .boehm_leak && (node.args[0].typ != ast.string_type + || node.args[0].expr !in [ast.Ident, ast.StringLiteral, ast.SelectorExpr, ast.ComptimeSelector]) { + w.uses_free[ast.string_type] = true + } } else if node.is_method && node.name == 'str' { w.uses_str[node.left_type] = true } else if node.is_method && node.name == 'free' { @@ -1429,6 +1435,11 @@ fn (mut w Walker) mark_resource_dependencies() { string_idx_str := ast.string_type_idx.str() array_idx_str := ast.array_type_idx.str() + if w.pref.gc_mode == .boehm_leak { + // `-gc boehm_leak` emits scope-exit frees for used heap-backed values. + w.uses_free[ast.string_type] = true + } + if w.trace_enabled { eprintln('>>>>>>>>>> DEPS USAGE') } -- 2.39.5