From 7d6fd95995c08b27f8ddf5d469c9cdd889d10154 Mon Sep 17 00:00:00 2001 From: GGRei Date: Fri, 12 Jun 2026 01:29:25 +0200 Subject: [PATCH] v2: fix cleanc array contains and map push lowering (#27418) --- vlib/v2/builder/cleanc_target_e2e_test.v | 130 ++++++++++++++++++ vlib/v2/gen/cleanc/array.v | 21 +-- vlib/v2/gen/cleanc/cleanc.v | 74 +++++----- vlib/v2/transformer/flat_write.v | 7 + vlib/v2/transformer/transformer.v | 8 +- .../transformer/transformer_flat_diff_test.v | 12 ++ vlib/v2/transformer/transformer_test.v | 118 ++++++++++++++++ 7 files changed, 326 insertions(+), 44 deletions(-) diff --git a/vlib/v2/builder/cleanc_target_e2e_test.v b/vlib/v2/builder/cleanc_target_e2e_test.v index 6175fbc04..6ba74f996 100644 --- a/vlib/v2/builder/cleanc_target_e2e_test.v +++ b/vlib/v2/builder/cleanc_target_e2e_test.v @@ -373,6 +373,133 @@ fn assert_no_obvious_hosted_headers(c_source string) { } } +fn c_source_line_matches(c_source string, idx int, line string) bool { + end_idx := idx + line.len + return (idx == 0 || c_source[idx - 1] == `\n`) + && (end_idx == c_source.len || c_source[end_idx] == `\n`) +} + +fn c_source_find_complete_line(c_source string, line string) ?int { + mut search_from := 0 + for { + idx := c_source.index_after(line, search_from) or { return none } + if c_source_line_matches(c_source, idx, line) { + return idx + } + search_from = idx + 1 + } + return none +} + +fn c_source_count_complete_lines(c_source string, line string) int { + mut count := 0 + mut search_from := 0 + for { + idx := c_source.index_after(line, search_from) or { break } + if c_source_line_matches(c_source, idx, line) { + count++ + } + search_from = idx + 1 + } + return count +} + +fn c_source_find_line_prefix(c_source string, prefix string) ?int { + mut search_from := 0 + for { + idx := c_source.index_after(prefix, search_from) or { return none } + if idx == 0 || c_source[idx - 1] == `\n` { + return idx + } + search_from = idx + 1 + } + return none +} + +fn c_source_find_line_prefix_after(c_source string, prefix string, start int) ?int { + mut search_from := start + for { + idx := c_source.index_after(prefix, search_from) or { return none } + if idx == 0 || c_source[idx - 1] == `\n` { + return idx + } + search_from = idx + 1 + } + return none +} + +fn assert_array_contains_fallback_decl_order(c_source string, fn_name string, prototype string) { + prototype_count := c_source_count_complete_lines(c_source, prototype) + assert prototype_count == 1, 'expected exactly one complete-line fallback prototype `${prototype}`, found ${prototype_count}' + proto_idx := c_source_find_complete_line(c_source, prototype) or { + assert false, 'missing array contains fallback prototype `${prototype}`' + return + } + symbol := '${fn_name}(' + first_symbol_idx := c_source.index(symbol) or { + assert false, 'missing array contains fallback symbol `${symbol}`' + return + } + assert first_symbol_idx == proto_idx + 'bool '.len, '`${fn_name}` appears before its fallback prototype' + + first_use_idx := c_source.index_after(symbol, proto_idx + prototype.len) or { + assert false, 'missing array contains fallback use `${symbol}` after prototype' + return + } + assert proto_idx < first_use_idx, '`${fn_name}` fallback prototype must precede first use' + body_prefix := 'bool ${fn_name}(' + if body_idx := c_source_find_line_prefix_after(c_source, body_prefix, proto_idx + prototype.len) { + assert first_use_idx < body_idx, '`${fn_name}` first use should occur before generated body' + } + weak_body := '__attribute__((weak)) bool ${fn_name}(' + if weak_idx := c_source_find_line_prefix(c_source, weak_body) { + assert first_use_idx < weak_idx, '`${fn_name}` first use should occur before fallback weak body' + assert proto_idx < weak_idx, '`${fn_name}` fallback prototype must precede weak body' + } +} + +fn test_cleanc_cli_array_contains_fallback_decls_precede_pass5_uses() { + tmp_dir := os.join_path(os.vtmp_dir(), 'v2_cleanc_array_contains_fallback_${os.getpid()}') + os.rmdir_all(tmp_dir) or {} + os.mkdir_all(tmp_dir) or { panic(err) } + defer { + os.rmdir_all(tmp_dir) or {} + } + v2_binary := build_v2_for_target_e2e(tmp_dir) + res := run_v2_to_c_project_files(v2_binary, tmp_dir, 'array_contains_fallback', [], { + 'ssa/ids.v': 'module ssa + +pub type ValueID = int +pub type BlockID = int +' + 'types/types.v': 'module types + +pub type Type = int +' + 'main.v': 'module main + +import ssa +import types + +fn main() { + values := [ssa.ValueID(1)] + blocks := [ssa.BlockID(2)] + type_ids := [types.Type(3)] + assert values.contains(ssa.ValueID(1)) + assert blocks.contains(ssa.BlockID(2)) + assert type_ids.contains(types.Type(3)) +} +' + }, 'main.v') + assert_cli_success(res) + assert_array_contains_fallback_decl_order(res.c_source, 'Array_ssa__ValueID_contains', + 'bool Array_ssa__ValueID_contains(Array_ssa__ValueID a, ssa__ValueID v);') + assert_array_contains_fallback_decl_order(res.c_source, 'Array_ssa__BlockID_contains', + 'bool Array_ssa__BlockID_contains(Array_ssa__BlockID a, ssa__BlockID v);') + assert_array_contains_fallback_decl_order(res.c_source, 'Array_types__Type_contains', + 'bool Array_types__Type_contains(Array_types__Type a, types__Type v);') +} + fn test_cleanc_cli_generated_c_target_matrix() { tmp_dir := os.join_path(os.vtmp_dir(), 'v2_cleanc_target_e2e_${os.getpid()}') os.rmdir_all(tmp_dir) or {} @@ -1233,6 +1360,9 @@ fn main() { ') assert_generated_c_heap_runtime_static_assert_contains(map_value_array_append_missing_runtime_res, 'map__get_and_set') + assert map_value_array_append_missing_runtime_res.c_source.contains('__new_array_with_default_noscan(0, 0, sizeof(int), NULL)'), map_value_array_append_missing_runtime_res.c_source + assert !map_value_array_append_missing_runtime_res.c_source.contains('map__get(&m'), map_value_array_append_missing_runtime_res.c_source + assert !map_value_array_append_missing_runtime_res.c_source.contains('map__get(&(m)'), map_value_array_append_missing_runtime_res.c_source array_clone_missing_runtime_res := run_v2_to_c(v2_binary, tmp_dir, 'freestanding_array_clone_missing_runtime', [ diff --git a/vlib/v2/gen/cleanc/array.v b/vlib/v2/gen/cleanc/array.v index 0de5c7ddf..95d4f0b82 100644 --- a/vlib/v2/gen/cleanc/array.v +++ b/vlib/v2/gen/cleanc/array.v @@ -4,6 +4,7 @@ module cleanc +import strings import v2.ast import v2.types @@ -264,20 +265,21 @@ fn (g &Gen) missing_array_contains_fallback_specs() []ArrayContainsFallbackSpec return specs } -fn (mut g Gen) emit_missing_array_contains_fallback_decls() { - specs := g.missing_array_contains_fallback_specs() +fn array_contains_fallback_decls(specs []ArrayContainsFallbackSpec) string { if specs.len == 0 { - return + return '' } + mut sb := strings.new_builder(specs.len * 80) for spec in specs { - g.sb.writeln('bool ${spec.fn_name}(${spec.arr_type} a, ${spec.elem_type} v);') + sb.writeln('bool ${spec.fn_name}(${spec.arr_type} a, ${spec.elem_type} v);') } - g.sb.writeln('') + sb.writeln('') + return sb.str() } -fn (mut g Gen) emit_missing_array_contains_fallbacks() { +fn (mut g Gen) emit_array_contains_fallbacks(specs []ArrayContainsFallbackSpec) { mut emitted_any := false - for spec in g.missing_array_contains_fallback_specs() { + for spec in specs { fn_key := 'fn_${spec.fn_name}' if fn_key in g.emitted_types { continue @@ -578,6 +580,9 @@ fn (mut g Gen) gen_map_index_array_append(lhs ast.IndexExpr, rhs ast.Expr, elem_ if !ok { return false } + if value_type.trim_space().ends_with('*') { + return false + } value_base := value_type.trim_right('*') mut is_array_value := c_type_is_array_value(value_base) if !is_array_value { @@ -596,7 +601,7 @@ fn (mut g Gen) gen_map_index_array_append(lhs ast.IndexExpr, rhs ast.Expr, elem_ g.tmp_counter++ g.sb.write_string('({ ${key_type} ${key_tmp} = ') g.expr(lhs.expr) - g.sb.write_string('; ${value_type} ${zero_tmp} = (${value_type}){0}; ${value_type}* ${arr_tmp} = (${value_type}*)map__get(') + g.sb.write_string('; ${value_type} ${zero_tmp} = __new_array_with_default_noscan(0, 0, sizeof(${elem_type}), NULL); ${value_type}* ${arr_tmp} = (${value_type}*)map__get_and_set(') if lhs_is_ptr { g.expr(lhs.lhs) } else { diff --git a/vlib/v2/gen/cleanc/cleanc.v b/vlib/v2/gen/cleanc/cleanc.v index d227f5060..c0cb623b9 100644 --- a/vlib/v2/gen/cleanc/cleanc.v +++ b/vlib/v2/gen/cleanc/cleanc.v @@ -1423,7 +1423,6 @@ pub fn (mut g Gen) gen_passes_1_to_4() { g.emit_interface_method_wrapper_decls() g.emit_interface_clone_decls() g.emit_array_interface_repeat_decls() - g.emit_missing_array_contains_fallback_decls() stage_start = g.mark_cgen_step(stats_enabled, stats_scope, mut stats_sw, stage_start, 'pass 4 helper declarations') @@ -2102,7 +2101,9 @@ pub fn (mut g Gen) gen_finalize() string { stage_start = g.mark_cgen_step(stats_enabled, stats_scope, mut stats_sw, stage_start, 'finalize test main') - g.emit_missing_array_contains_fallbacks() + array_contains_specs := g.missing_array_contains_fallback_specs() + late_array_contains_decls := array_contains_fallback_decls(array_contains_specs) + g.emit_array_contains_fallbacks(array_contains_specs) g.emit_missing_runtime_fallbacks() g.emit_cached_module_init_function() g.emit_exported_const_symbols() @@ -2121,41 +2122,50 @@ pub fn (mut g Gen) gen_finalize() string { } stage_start = g.mark_cgen_step(stats_enabled, stats_scope, mut stats_sw, stage_start, 'finalize late str macros') - if g.anon_fn_defs.len > 0 || g.spawn_wrapper_defs.len > 0 || g.trampoline_defs.len > 0 - || g.late_struct_defs.len > 0 || g.pending_late_body_keys.len > 0 { + has_late_defs := g.anon_fn_defs.len > 0 || g.spawn_wrapper_defs.len > 0 + || g.trampoline_defs.len > 0 || g.late_struct_defs.len > 0 + || g.pending_late_body_keys.len > 0 + if late_array_contains_decls.len > 0 || has_late_defs { full := g.sb.str() stage_start = g.mark_cgen_step(stats_enabled, stats_scope, mut stats_sw, stage_start, 'finalize snapshot') - mut out_sb := strings.new_builder(full.len + 4096) + mut out_sb := strings.new_builder(full.len + late_array_contains_decls.len + 4096) unsafe { out_sb.write_ptr(full.str, g.pass5_start_pos) } - // Late-discovered generic struct definitions (discovered during setup/pass 4 codegen) - for def in g.late_struct_defs { - out_sb.write_string(def) - } - // Mark pending late body keys as emitted now that they're in the output. - for key, _ in g.pending_late_body_keys { - g.emitted_types[key] = true - } - g.pending_late_body_keys = map[string]bool{} - // Emit any option/result wrappers that were deferred because their payload - // types were only in late_struct_defs. Temporarily swap sb with out_sb. - g.sb = out_sb - g.emit_option_result_structs() - out_sb = g.sb - g.sb = strings.new_builder(0) - mut seen_spawn_defs := map[string]bool{} - for def in g.spawn_wrapper_defs { - if def in seen_spawn_defs { - continue + if has_late_defs { + // Late-discovered generic struct definitions (discovered during setup/pass 4 codegen) + for def in g.late_struct_defs { + out_sb.write_string(def) + } + // Mark pending late body keys as emitted now that they're in the output. + for key, _ in g.pending_late_body_keys { + g.emitted_types[key] = true + } + g.pending_late_body_keys = map[string]bool{} + // Emit any option/result wrappers that were deferred because their payload + // types were only in late_struct_defs. Temporarily swap sb with out_sb. + g.sb = out_sb + g.emit_option_result_structs() + out_sb = g.sb + g.sb = strings.new_builder(0) + } + if late_array_contains_decls.len > 0 { + out_sb.write_string(late_array_contains_decls) + } + if has_late_defs { + mut seen_spawn_defs := map[string]bool{} + for def in g.spawn_wrapper_defs { + if def in seen_spawn_defs { + continue + } + seen_spawn_defs[def] = true + out_sb.write_string(def) + } + for def in g.anon_fn_defs { + out_sb.write_string(def) + } + for def in g.trampoline_defs { + out_sb.write_string(def) } - seen_spawn_defs[def] = true - out_sb.write_string(def) - } - for def in g.anon_fn_defs { - out_sb.write_string(def) - } - for def in g.trampoline_defs { - out_sb.write_string(def) } if g.pass5_start_pos < full.len { unsafe { out_sb.write_ptr(full.str + g.pass5_start_pos, full.len - g.pass5_start_pos) } diff --git a/vlib/v2/transformer/flat_write.v b/vlib/v2/transformer/flat_write.v index df069380b..9a0807777 100644 --- a/vlib/v2/transformer/flat_write.v +++ b/vlib/v2/transformer/flat_write.v @@ -2120,6 +2120,13 @@ fn (mut t Transformer) transform_stmt_list_item_cursor_to_flat(c ast.Cursor, mut t.count_flat_fallback('stmt_expr') t.transform_stmt_list_item_to_flat(expr_stmt_from_cursor(c), mut ids, mut out) } else { + expr := c.edge(0) + if expr.kind() == .expr_infix + && unsafe { token.Token(int(expr.aux())) } == .left_shift { + if t.try_emit_map_index_push_to_flat(expr_stmt_from_cursor(c), mut ids, mut out) { + return + } + } id := t.transform_expr_stmt_cursor_to_flat(c, mut out) t.append_transformed_stmt_id_to_flat(mut ids, id, mut out) } diff --git a/vlib/v2/transformer/transformer.v b/vlib/v2/transformer/transformer.v index cdbf884fc..21e90e8a9 100644 --- a/vlib/v2/transformer/transformer.v +++ b/vlib/v2/transformer/transformer.v @@ -7171,13 +7171,13 @@ fn (mut t Transformer) try_transform_map_index_push(stmt ast.ExprStmt) ?ast.Stmt if infix.op != .left_shift { return none } - if infix.lhs !is ast.IndexExpr { - return none - } - index_expr := infix.lhs as ast.IndexExpr + index_expr := t.index_expr_from_or_target(infix.lhs) or { return none } // Check if the indexed expression is a map with array value type map_expr_typ := t.map_index_lhs_type(index_expr.lhs) or { return none } map_type := t.unwrap_map_type(map_expr_typ) or { return none } + if t.is_pointer_type(map_type.value_type) { + return none + } // Map values can be aliases of arrays (for example strings.Builder). val_type := t.unwrap_alias_and_pointer_type(map_type.value_type) if val_type !is types.Array { diff --git a/vlib/v2/transformer/transformer_flat_diff_test.v b/vlib/v2/transformer/transformer_flat_diff_test.v index 2089c46f6..bd1e79f14 100644 --- a/vlib/v2/transformer/transformer_flat_diff_test.v +++ b/vlib/v2/transformer/transformer_flat_diff_test.v @@ -707,6 +707,13 @@ fn use_for_in_map() int { } ' +const fixture_map_index_array_push = ' +fn use_map_index_array_push() { + mut m := map[int][]int{} + m[1] << 2 +} +' + const fixture_return_match = ' fn classify(n int) string { return match n { @@ -1880,6 +1887,11 @@ fn test_flat_input_to_flat_direct_monomorphizes_generics_like_file_input() { fixture_generic_fn) } +fn test_flat_input_to_flat_direct_map_index_array_push() { + run_flat_input_to_flat_direct_matches_file_input_direct('flat_input_direct_map_index_array_push', + fixture_map_index_array_push) +} + fn test_to_flat_direct_parity_for_in_map() { run_to_flat_direct_parity('to_flat_direct_for_in_map', fixture_for_in_map) } diff --git a/vlib/v2/transformer/transformer_test.v b/vlib/v2/transformer/transformer_test.v index 2a73a5d31..038395c1c 100644 --- a/vlib/v2/transformer/transformer_test.v +++ b/vlib/v2/transformer/transformer_test.v @@ -6005,6 +6005,124 @@ fn test_transform_map_index_push_lowers_to_map_get_and_set() { assert map_call.args.len == 3 } +fn test_transform_map_index_push_generic_arg_or_index_lowers_to_map_get_and_set() { + mut t := create_transformer_with_vars({ + 'lists': types.Type(types.Map{ + key_type: types.int_ + value_type: types.Type(types.Array{ + elem_type: types.int_ + }) + }) + }) + + result := t.try_transform_map_index_push(ast.ExprStmt{ + expr: ast.InfixExpr{ + op: .left_shift + lhs: ast.GenericArgOrIndexExpr{ + lhs: ast.Ident{ + name: 'lists' + } + expr: ast.BasicLiteral{ + kind: .number + value: '2' + } + } + rhs: ast.BasicLiteral{ + kind: .number + value: '1' + } + } + }) or { + assert false, 'expected ambiguous map index push to be transformed' + return + } + + assert result is ast.ExprStmt + expr_stmt := result as ast.ExprStmt + assert expr_stmt.expr is ast.CallExpr + mut call_names := []string{} + collect_call_names_from_expr(expr_stmt.expr, mut call_names) + assert 'map__get_and_set' in call_names +} + +fn test_transform_map_index_push_generic_args_lowers_to_map_get_and_set() { + mut t := create_transformer_with_vars({ + 'lists': types.Type(types.Map{ + key_type: types.int_ + value_type: types.Type(types.Array{ + elem_type: types.int_ + }) + }) + }) + + result := t.try_transform_map_index_push(ast.ExprStmt{ + expr: ast.InfixExpr{ + op: .left_shift + lhs: ast.GenericArgs{ + lhs: ast.Ident{ + name: 'lists' + } + args: [ + ast.Expr(ast.BasicLiteral{ + kind: .number + value: '2' + }), + ] + } + rhs: ast.BasicLiteral{ + kind: .number + value: '1' + } + } + }) or { + assert false, 'expected generic-args map index push to be transformed' + return + } + + assert result is ast.ExprStmt + expr_stmt := result as ast.ExprStmt + assert expr_stmt.expr is ast.CallExpr + mut call_names := []string{} + collect_call_names_from_expr(expr_stmt.expr, mut call_names) + assert 'map__get_and_set' in call_names +} + +fn test_transform_map_index_push_refuses_pointer_map_value() { + array_int_type := types.Type(types.Array{ + elem_type: types.int_ + }) + mut t := create_transformer_with_vars({ + 'lists': types.Type(types.Map{ + key_type: types.int_ + value_type: types.Type(types.Pointer{ + base_type: array_int_type + }) + }) + }) + + if _ := t.try_transform_map_index_push(ast.ExprStmt{ + expr: ast.InfixExpr{ + op: .left_shift + lhs: ast.IndexExpr{ + lhs: ast.Ident{ + name: 'lists' + } + expr: ast.BasicLiteral{ + kind: .number + value: '2' + } + } + rhs: ast.BasicLiteral{ + kind: .number + value: '1' + } + } + }) + { + assert false, 'expected pointer map value push not to be transformed' + } +} + fn test_transform_map_index_selector_postfix_lowers_to_map_get_and_set() { fn_type := types.Type(types.Struct{ name: 'Fn' -- 2.39.5