From 82e947b1b9e56b6ae3f376c0c2880c4351fff749 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sat, 27 Jun 2026 12:05:31 +0300 Subject: [PATCH] cgen: stabilize auxiliary type symbol hashes (#27570) --- vlib/v/gen/c/array.v | 52 +++++++----- vlib/v/gen/c/cgen.v | 31 +++----- vlib/v/gen/c/coutput_test.v | 84 ++++++++++++++++++++ vlib/v/gen/c/utils.v | 154 ++++++++++++++++++++++++++++++++++++ 4 files changed, 281 insertions(+), 40 deletions(-) diff --git a/vlib/v/gen/c/array.v b/vlib/v/gen/c/array.v index 92d8a70b0..31b5872b8 100644 --- a/vlib/v/gen/c/array.v +++ b/vlib/v/gen/c/array.v @@ -1229,6 +1229,17 @@ fn (mut g Gen) gen_array_sorted(node ast.CallExpr) { g.writeln(';') } +// array_sort_expr_key returns a C-identifier-safe, lossless key for a sort expression. +fn array_sort_expr_key(expr_key string) string { + hex_digits := '0123456789abcdef' + mut b := strings.new_builder(expr_key.len * 2) + for ch in expr_key.bytes() { + b.write_u8(hex_digits[int(ch >> 4)]) + b.write_u8(hex_digits[int(ch & 0x0f)]) + } + return b.str() +} + // `users.sort(a.age < b.age)` fn (mut g Gen) gen_array_sort(node ast.CallExpr) { // println('filter s="${s}"') @@ -1251,7 +1262,8 @@ fn (mut g Gen) gen_array_sort(node ast.CallExpr) { // `users.sort(a.age > b.age)` // Generate a comparison function for a custom type elem_stype := g.styp(elem_type) - mut compare_fn := 'compare_${g.unique_file_path_hash}_${elem_stype.replace('*', '_ptr')}' + mut compare_fn := 'compare_${g.stable_type_symbol_hash(elem_type)}_${elem_stype.replace('*', + '_ptr')}' mut comparison_type := g.unwrap(ast.void_type) mut left_expr, mut right_expr := '', '' mut use_lambda := false @@ -1259,13 +1271,6 @@ fn (mut g Gen) gen_array_sort(node ast.CallExpr) { // the only argument can only be an infix expression like `a < b` or `b.field > a.field` if node.args.len == 0 { comparison_type = g.unwrap(elem_type.set_nr_muls(0)) - rlock g.array_sort_fn { - if compare_fn in g.array_sort_fn { - g.gen_array_sort_call(node, - g.ensure_array_sort_qsort_adapter(compare_fn, elem_type), left_is_array) - return - } - } left_expr = '*a' right_expr = '*b' } else if node.args[0].expr is ast.LambdaExpr { @@ -1288,23 +1293,18 @@ fn (mut g Gen) gen_array_sort(node ast.CallExpr) { } comparison_type = g.unwrap(comparison_left_type.set_nr_muls(0)) left_name := infix_expr.left.str() + expr_key := '${left_name}\n${infix_expr.op.str()}\n${infix_expr.right.str()}' if left_name.len > 1 { compare_fn += '_by' + left_name[1..].replace_each(['.', '_', '[', '_', ']', '_', "'", '_', '"', '_', '(', '', ')', '', ',', '', '/', '_']) } + compare_fn += '_expr_${array_sort_expr_key(expr_key)}' // is_reverse is `true` for `.sort(a > b)` and `.sort(b < a)` is_reverse := (left_name.starts_with('a') && infix_expr.op == .gt) || (left_name.starts_with('b') && infix_expr.op == .lt) if is_reverse { compare_fn += '_reverse' } - rlock g.array_sort_fn { - if compare_fn in g.array_sort_fn { - g.gen_array_sort_call(node, - g.ensure_array_sort_qsort_adapter(compare_fn, elem_type), left_is_array) - return - } - } if left_name.starts_with('a') != is_reverse { left_expr = g.expr_string(infix_expr.left) right_expr = g.expr_string(infix_expr.right) @@ -1328,8 +1328,16 @@ fn (mut g Gen) gen_array_sort(node ast.CallExpr) { // Register a new custom `compare_xxx` function for qsort() // TODO: move to checker + mut already_generated := false lock g.array_sort_fn { - g.array_sort_fn << compare_fn + already_generated = compare_fn in g.array_sort_fn + if !already_generated { + g.array_sort_fn << compare_fn + } + } + if already_generated { + g.gen_array_sort_call(node, '${compare_fn}_qsort_adapter', left_is_array) + return } stype_arg := g.styp(elem_type) @@ -1380,13 +1388,15 @@ fn (g &Gen) array_sort_fn_visibility() string { fn (mut g Gen) ensure_array_sort_qsort_adapter(compare_fn string, elem_type ast.Type) string { qsort_compare_fn := '${compare_fn}_qsort_adapter' - rlock g.array_sort_wrappers { - if qsort_compare_fn in g.array_sort_wrappers { - return qsort_compare_fn + mut already_generated := false + lock g.array_sort_wrappers { + already_generated = qsort_compare_fn in g.array_sort_wrappers + if !already_generated { + g.array_sort_wrappers << qsort_compare_fn } } - lock g.array_sort_wrappers { - g.array_sort_wrappers << qsort_compare_fn + if already_generated { + return qsort_compare_fn } elem_stype := g.styp(elem_type) g.sort_fn_definitions.writeln('${g.array_sort_fn_visibility()}int ${qsort_compare_fn}(const void* a, const void* b) {') diff --git a/vlib/v/gen/c/cgen.v b/vlib/v/gen/c/cgen.v index 0c583212b..fec6b4bd4 100644 --- a/vlib/v/gen/c/cgen.v +++ b/vlib/v/gen/c/cgen.v @@ -140,9 +140,7 @@ mut: tmp_var_ptr map[string]bool // indicates if the tmp var passed to or_block() is a ptr labeled_loops map[string]&ast.Stmt contains_ptr_cache map[ast.Type]bool - boehm_keep_decl map[string]bool - boehm_keep_gen map[string]bool - boehm_keep_busy map[string]bool + boehm_keep_gen shared map[string]bool inner_loop &ast.Stmt = unsafe { nil } cur_indexexpr []int // list of nested indexexpr which generates array_set/map_set shareds map[int]string // types with hidden mutex for which decl has been emitted @@ -433,9 +431,7 @@ pub fn gen(files []&ast.File, mut table ast.Table, pref_ &pref.Preferences) GenO has_debugger: 'v.debug' in table.modules reflection_strings: &reflection_strings generated_map_key_fns: map[ast.Type]bool{} - boehm_keep_decl: map[string]bool{} boehm_keep_gen: map[string]bool{} - boehm_keep_busy: map[string]bool{} closure_frame_arg_tmps: map[int]string{} generic_parts_cache: []i8{len: table.type_symbols.len} unwrap_generic_cache: map[u64]ast.Type{} @@ -1092,9 +1088,7 @@ fn cgen_process_one_file_cb(mut p pool.PoolProcessor, idx int, wid int) voidptr has_debugger: 'v.debug' in global_g.table.modules reflection_strings: global_g.reflection_strings generated_map_key_fns: map[ast.Type]bool{} - boehm_keep_decl: map[string]bool{} - boehm_keep_gen: map[string]bool{} - boehm_keep_busy: map[string]bool{} + boehm_keep_gen: global_g.boehm_keep_gen closure_frame_arg_tmps: map[int]string{} generic_parts_cache: []i8{len: global_g.table.type_symbols.len} unwrap_generic_cache: map[u64]ast.Type{} @@ -8681,18 +8675,19 @@ fn (mut g Gen) boehm_collect_keep_alive_helper_name(typ ast.Type) string { if styp.ends_with('_ptr') { return '' } - fn_name := '__v_boehm_collect_keepalive_${g.unique_file_path_hash}_${styp.replace('*', '_ptr').replace(' ', '_')}' - if g.boehm_keep_gen[fn_name] { - return fn_name - } - if !g.boehm_keep_decl[fn_name] { - g.definitions.writeln('static inline int ${fn_name}(${styp}* it, voidptr* out, int idx);') - g.boehm_keep_decl[fn_name] = true + fn_name := '__v_boehm_collect_keepalive_${g.stable_type_symbol_hash(resolved_typ)}_${styp.replace('*', + '_ptr').replace(' ', '_')}' + mut should_generate := false + lock g.boehm_keep_gen { + if fn_name !in g.boehm_keep_gen { + g.boehm_keep_gen[fn_name] = true + should_generate = true + } } - if g.boehm_keep_busy[fn_name] { + if !should_generate { return fn_name } - g.boehm_keep_busy[fn_name] = true + g.definitions.writeln('static inline int ${fn_name}(${styp}* it, voidptr* out, int idx);') mut sb := strings.new_builder(256) sb.writeln('static inline int ${fn_name}(${styp}* it, voidptr* out, int idx) {') match sym.kind { @@ -8776,8 +8771,6 @@ fn (mut g Gen) boehm_collect_keep_alive_helper_name(typ ast.Type) string { sb.writeln('}') g.auto_fn_definitions << sb.str() - g.boehm_keep_gen[fn_name] = true - g.boehm_keep_busy.delete(fn_name) return fn_name } diff --git a/vlib/v/gen/c/coutput_test.v b/vlib/v/gen/c/coutput_test.v index 1a51494a8..9faa2436d 100644 --- a/vlib/v/gen/c/coutput_test.v +++ b/vlib/v/gen/c/coutput_test.v @@ -633,6 +633,90 @@ fn test_array_sort_with_compare_uses_stable_sort_adapters() { assert normalized.contains('_qsort_adapter);') } +fn test_array_sort_expression_key_avoids_sanitized_name_collisions() { + os.chdir(vroot) or {} + test_dir := os.join_path(os.vtmp_dir(), 'coutput_array_sort_expr_collision_${os.getpid()}') + os.mkdir_all(test_dir)! + defer { + os.rmdir_all(test_dir) or {} + } + os.write_file(os.join_path(test_dir, 'main.v'), + ['module main', '', 'struct Inner {', '\tbar int', '}', '', 'struct Item {', '\tfoo Inner', '\tfoo_bar int', '\tlabel string', '}', '', 'fn main() {', '\tprintln(sort_by_nested())', '\tprintln(sort_by_flat())', '}'].join('\n') + + '\n')! + os.write_file(os.join_path(test_dir, 'nested.v'), + ['module main', '', 'fn sort_by_nested() string {', "\tmut items := [Item{ foo: Inner{ bar: 2 }, foo_bar: 1, label: 'nested-wrong' }, Item{ foo: Inner{ bar: 1 }, foo_bar: 2, label: 'nested-ok' }]", '\titems.sort(a.foo.bar < b.foo.bar)', '\treturn items[0].label', '}'].join('\n') + + '\n')! + os.write_file(os.join_path(test_dir, 'flat.v'), + ['module main', '', 'fn sort_by_flat() string {', "\tmut items := [Item{ foo: Inner{ bar: 1 }, foo_bar: 2, label: 'flat-wrong' }, Item{ foo: Inner{ bar: 2 }, foo_bar: 1, label: 'flat-ok' }]", '\titems.sort(a.foo_bar < b.foo_bar)', '\treturn items[0].label', '}'].join('\n') + + '\n')! + pexe := os.join_path(test_dir, 'sort_expr_collision') + cmd := '${os.quoted_path(vexe)} -o ${os.quoted_path(pexe)} ${os.quoted_path(test_dir)}' + compilation := os.execute(cmd) + ensure_compilation_succeeded(compilation, cmd) + res := os.execute(os.quoted_path(pexe)) + assert res.exit_code == 0 + assert res.output.trim_space().replace('\r\n', '\n') == 'nested-ok\nflat-ok' +} + +// generated_c_symbol_with_prefix extracts one generated C symbol from compiler output. +fn generated_c_symbol_with_prefix(output string, prefix string) string { + idx := output.index(prefix) or { return '' } + rest := output[idx..] + mut end := 0 + for end < rest.len && (rest[end].is_letter() || rest[end].is_digit() || rest[end] == `_`) { + end++ + } + return rest[..end] +} + +fn test_auxiliary_c_symbols_use_stable_type_hashes() { + os.chdir(vroot) or {} + test_dir := os.join_path(os.vtmp_dir(), 'coutput_stable_type_symbol_hash_${os.getpid()}') + dir_a := os.join_path(test_dir, 'a') + dir_b := os.join_path(test_dir, 'b') + os.mkdir_all(dir_a)! + os.mkdir_all(dir_b)! + defer { + os.rmdir_all(test_dir) or {} + } + source_lines := [ + 'module main', + '', + 'struct Item {', + '\tname string', + '\trank int', + '}', + '', + 'fn main() {', + "\tmut items := [Item{'b', 2}, Item{'a', 1}, Item{'c', 3}]", + '\titems.sort(a.rank < b.rank)', + '\tmut nested := [items]', + '\tprintln("\${nested[0][0].name}")', + '}', + ] + source := source_lines.join('\n') + '\n' + path_a := os.join_path(dir_a, 'main.v') + path_b := os.join_path(dir_b, 'main.v') + os.write_file(path_a, source)! + os.write_file(path_b, source)! + cmd_a := '${os.quoted_path(vexe)} -prod -gc boehm_full_opt -o - ${os.quoted_path(path_a)}' + cmd_b := '${os.quoted_path(vexe)} -prod -gc boehm_full_opt -o - ${os.quoted_path(path_b)}' + compilation_a := os.execute(cmd_a) + compilation_b := os.execute(cmd_b) + ensure_compilation_succeeded(compilation_a, cmd_a) + ensure_compilation_succeeded(compilation_b, cmd_b) + compare_a := generated_c_symbol_with_prefix(compilation_a.output, 'compare_') + compare_b := generated_c_symbol_with_prefix(compilation_b.output, 'compare_') + keepalive_a := generated_c_symbol_with_prefix(compilation_a.output, + '__v_boehm_collect_keepalive_') + keepalive_b := generated_c_symbol_with_prefix(compilation_b.output, + '__v_boehm_collect_keepalive_') + assert compare_a != '' + assert keepalive_a != '' + assert compare_a == compare_b + assert keepalive_a == keepalive_b +} + fn test_veb_implicit_ctx_alias_uses_user_context_name() { os.chdir(vroot) or {} test_source := os.join_path(os.vtmp_dir(), 'coutput_veb_implicit_ctx_alias.vv') diff --git a/vlib/v/gen/c/utils.v b/vlib/v/gen/c/utils.v index 24b2d4a31..079e529f8 100644 --- a/vlib/v/gen/c/utils.v +++ b/vlib/v/gen/c/utils.v @@ -3,6 +3,7 @@ // that can be found in the LICENSE file. module c +import hash.fnv1a import v.ast const cgen_resolution_hash_prime = u64(1099511628211) @@ -10,6 +11,159 @@ const cgen_resolution_hash_seed = u64(14695981039346656037) const cgen_unwrap_generic_cache_salt = u64(0x9e3779b185ebca87) const cgen_scope_var_type_cache_salt = u64(0xc2b2ae3d27d4eb4f) +// stable_type_symbol_hash returns a deterministic hash for generated helper names. +fn (mut g Gen) stable_type_symbol_hash(typ ast.Type) u64 { + mut seen := map[string]bool{} + return fnv1a.sum64_string(g.stable_type_symbol_key(typ, mut seen)) +} + +// stable_type_symbol_key builds a stable type descriptor without using table registration ids. +fn (mut g Gen) stable_type_symbol_key(typ ast.Type, mut seen map[string]bool) string { + if typ == 0 { + return '0' + } + mut resolved_typ := g.unwrap_generic(g.recheck_concrete_type(typ)) + if resolved_typ == 0 { + resolved_typ = g.unwrap_generic(typ) + } + if resolved_typ == 0 { + return '0' + } + sym := g.table.final_sym(resolved_typ) + type_name := g.table.type_to_str(resolved_typ) + styp := g.styp(resolved_typ) + flags := stable_type_symbol_flags(resolved_typ) + base := '${sym.kind}:${type_name}:${styp}:${flags}' + if resolved_typ.nr_muls() > 0 || resolved_typ.is_any_kind_of_pointer() + || resolved_typ.has_option_or_result() { + return base + } + if seen[base] { + return 'recursive:${base}' + } + seen[base] = true + mut parts := [base] + match sym.info { + ast.Alias { + parts << 'alias:${sym.info.language}:${g.stable_type_symbol_key(sym.info.parent_type, mut seen)}' + } + ast.Array { + parts << 'array:${sym.info.nr_dims}:${g.stable_type_symbol_key(sym.info.elem_type, mut seen)}' + } + ast.ArrayFixed { + parts << 'array_fixed:${sym.info.size}:${g.stable_type_symbol_key(sym.info.elem_type, mut seen)}' + } + ast.Chan { + parts << 'chan:${sym.info.is_mut}:${g.stable_type_symbol_key(sym.info.elem_type, mut seen)}' + } + ast.Enum { + parts << 'enum:${sym.info.vals.join(',')}:${g.stable_type_symbol_key(sym.info.typ, mut seen)}:${sym.info.is_flag}:${sym.info.is_multi_allowed}:${sym.info.is_typedef}' + } + ast.FnType { + parts << 'fn:${sym.info.func.is_variadic}:${g.stable_type_symbol_key(sym.info.func.return_type, mut seen)}' + for param in sym.info.func.params { + parts << 'param:${param.is_mut}:${param.is_shared}:${param.is_atomic}:${g.stable_type_symbol_key(param.typ, mut seen)}' + } + } + ast.GenericInst { + parent_name := g.table.type_to_str(ast.new_type(sym.info.parent_idx)) + parts << 'generic_inst:${parent_name}' + for concrete_type in sym.info.concrete_types { + parts << g.stable_type_symbol_key(concrete_type, mut seen) + } + } + ast.Interface { + parts << 'interface:${sym.info.is_generic}' + for embed in sym.info.embeds { + parts << 'embed:${g.stable_type_symbol_key(embed, mut seen)}' + } + for field in sym.info.fields { + parts << stable_type_symbol_field_key(mut g, field, mut seen) + } + for method in sym.info.methods { + parts << stable_type_symbol_method_key(mut g, method, mut seen) + } + } + ast.Map { + parts << 'map:${g.stable_type_symbol_key(sym.info.key_type, mut seen)}:${g.stable_type_symbol_key(sym.info.value_type, mut seen)}' + } + ast.MultiReturn { + for return_type in sym.info.types { + parts << 'return:${g.stable_type_symbol_key(return_type, mut seen)}' + } + } + ast.Struct { + parts << 'struct:${sym.info.is_typedef}:${sym.info.is_union}:${sym.info.is_heap}:${sym.info.is_shared}:${sym.info.is_generic}' + for embed in sym.info.embeds { + parts << 'embed:${g.stable_type_symbol_key(embed, mut seen)}' + } + for field in sym.info.fields { + parts << stable_type_symbol_field_key(mut g, field, mut seen) + } + } + ast.SumType { + parts << 'sum_type:${sym.info.is_anon}:${sym.info.is_generic}' + for variant in sym.info.variants { + parts << 'variant:${g.stable_type_symbol_key(variant, mut seen)}' + } + for field in sym.info.fields { + parts << stable_type_symbol_field_key(mut g, field, mut seen) + } + } + ast.Thread { + parts << 'thread:${g.stable_type_symbol_key(sym.info.return_type, mut seen)}' + } + else {} + } + + return parts.join('|') +} + +// stable_type_symbol_flags serializes the type flags that affect generated helper identity. +fn stable_type_symbol_flags(typ ast.Type) string { + mut flags := []string{cap: 8} + if typ.has_flag(.option) { + flags << 'option' + } + if typ.has_flag(.result) { + flags << 'result' + } + if typ.has_flag(.variadic) { + flags << 'variadic' + } + if typ.has_flag(.generic) { + flags << 'generic' + } + if typ.has_flag(.shared_f) { + flags << 'shared' + } + if typ.has_flag(.atomic_f) { + flags << 'atomic' + } + if typ.has_flag(.option_mut_param_t) { + flags << 'option_mut_param' + } + flags << 'muls:${typ.nr_muls()}' + return flags.join(',') +} + +// stable_type_symbol_field_key adds field identity and type information to a type descriptor. +fn stable_type_symbol_field_key(mut g Gen, field ast.StructField, mut seen map[string]bool) string { + return 'field:${field.name}:${field.is_embed}:${field.is_part_of_union}:${field.is_volatile}:${g.stable_type_symbol_key(field.typ, mut + seen)}' +} + +// stable_type_symbol_method_key adds method signature information to a type descriptor. +fn stable_type_symbol_method_key(mut g Gen, method ast.Fn, mut seen map[string]bool) string { + mut parts := [ + 'method:${method.name}:${method.is_variadic}:${g.stable_type_symbol_key(method.return_type, mut seen)}', + ] + for param in method.params { + parts << 'param:${param.is_mut}:${param.is_shared}:${param.is_atomic}:${g.stable_type_symbol_key(param.typ, mut seen)}' + } + return parts.join(',') +} + @[inline] fn cgen_resolution_hash_mix(key u64, value u64) u64 { return (key ^ value) * cgen_resolution_hash_prime -- 2.39.5