From 54660db7f5d8ed2f1a1a9fc79802517601eb30c3 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Thu, 23 Apr 2026 22:22:31 +0300 Subject: [PATCH] cgen: fix Cannot concatenate chars or runes into strings (fixes #17202) --- vlib/v/checker/assign.v | 15 ++- vlib/v/checker/infix.v | 116 +++++++++++------- vlib/v/gen/c/assign.v | 14 ++- vlib/v/gen/c/cgen.v | 19 +++ vlib/v/gen/c/infix.v | 21 +++- .../c/testdata/string_concat_char_rune.out | 6 + .../gen/c/testdata/string_concat_char_rune.vv | 16 +++ 7 files changed, 156 insertions(+), 51 deletions(-) create mode 100644 vlib/v/gen/c/testdata/string_concat_char_rune.out create mode 100644 vlib/v/gen/c/testdata/string_concat_char_rune.vv diff --git a/vlib/v/checker/assign.v b/vlib/v/checker/assign.v index 489e910af..c74ec6c22 100644 --- a/vlib/v/checker/assign.v +++ b/vlib/v/checker/assign.v @@ -947,12 +947,15 @@ or use an explicit `unsafe{ a[..] }`, if you do not want a copy of the slice.', } else { left_type } - if left_deref == ast.string_type { + if c.is_string_like_type(left_deref) { if node.op != .plus_assign { c.error('operator `${node.op}` not defined on left operand type `${left_sym.name}`', left.pos()) } - if right_type != ast.string_type { + if node.op == .plus_assign && !c.is_string_concat_type(right_type) { + c.error('invalid right operand: ${left_sym.name} ${node.op} ${right_sym.name}', + right.pos()) + } else if node.op != .plus_assign && !c.is_string_like_type(right_type) { c.error('invalid right operand: ${left_sym.name} ${node.op} ${right_sym.name}', right.pos()) } @@ -1136,8 +1139,12 @@ or use an explicit `unsafe{ a[..] }`, if you do not want a copy of the slice.', right.pos()) } // Dual sides check (compatibility check) - assign_right_type := if original_op in [.left_shift_assign, .right_shift_assign, - .unsigned_right_shift_assign] { + is_string_plus_assign := original_op == .plus_assign + && c.is_string_like_type(left_type_unwrapped) + && c.is_string_concat_type(right_type_unwrapped) + assign_right_type := if + original_op in [.left_shift_assign, .right_shift_assign, .unsigned_right_shift_assign] + || is_string_plus_assign { left_type_unwrapped } else { right_type_unwrapped diff --git a/vlib/v/checker/infix.v b/vlib/v/checker/infix.v index 5d746732c..6d887391d 100644 --- a/vlib/v/checker/infix.v +++ b/vlib/v/checker/infix.v @@ -25,6 +25,23 @@ fn (c &Checker) type_is_optionish(typ ast.Type, sym ast.TypeSymbol) bool { || (sym.kind == .alias && sym.info is ast.Alias && sym.info.parent_type.has_flag(.option)) } +fn (c &Checker) is_string_like_type(typ ast.Type) bool { + return !typ.has_option_or_result() && typ.clear_flags() == ast.string_type +} + +fn (c &Checker) is_char_or_rune_like_type(typ ast.Type) bool { + return !typ.has_option_or_result() && typ.clear_flags() in [ast.char_type, ast.rune_type] +} + +fn (c &Checker) is_string_concat_type(typ ast.Type) bool { + return c.is_string_like_type(typ) || c.is_char_or_rune_like_type(typ) +} + +fn (c &Checker) is_string_concat_pair(left ast.Type, right ast.Type) bool { + return c.is_string_concat_type(left) && c.is_string_concat_type(right) + && (c.is_string_like_type(left) || c.is_string_like_type(right)) +} + fn has_matching_reference_operator_overload(sym &ast.TypeSymbol, op string, receiver_type ast.Type, operand_type ast.Type) bool { method := sym.find_method_with_generic_parent(op) or { return false } return method.params.len == 2 && method.params[0].typ == receiver_type @@ -311,6 +328,9 @@ fn (mut c Checker) infix_expr(mut node ast.InfixExpr) ast.Type { } } mut return_type := left_type + if node.op == .plus && c.is_string_concat_pair(left_type, right_type) { + return_type = ast.string_type + } left_is_explicit_ptr := left_type.is_any_kind_of_pointer() && !node.left.is_auto_deref_var() && left_final_sym.kind != .voidptr right_is_explicit_ptr := right_type.is_any_kind_of_pointer() && !node.right.is_auto_deref_var() @@ -653,46 +673,57 @@ fn (mut c Checker) infix_expr(mut node ast.InfixExpr) ast.Type { } else { unaliased_left_type := c.table.unalias_num_type(unwrapped_left_type) unalias_right_type := c.table.unalias_num_type(unwrapped_right_type) - mut promoted_type := c.promote_keeping_aliases(unaliased_left_type, - unalias_right_type, left_sym.kind, right_sym.kind) - promoted_type = c.adjust_infix_int_literal_promotion(node.left, node.right, - unaliased_left_type, unalias_right_type, promoted_type) - // subtract pointers is allowed in unsafe block - is_allowed_pointer_arithmetic := left_type.is_any_kind_of_pointer() - && right_type.is_any_kind_of_pointer() && node.op == .minus - if is_allowed_pointer_arithmetic { - promoted_type = ast.int_type - } - if promoted_type.idx() == ast.void_type_idx { - left_name := c.table.type_to_str(unwrapped_left_type) - right_name := c.table.type_to_str(unwrapped_right_type) - c.error('mismatched types `${left_name}` and `${right_name}`', left_right_pos) - } else if promoted_type.has_option_or_result() { - s := c.table.type_to_str(promoted_type) - c.error('`${node.op}` cannot be used with `${s}`', node.pos) - } else if promoted_type.is_float() { - if node.op in [.mod, .xor, .amp, .pipe] { - side := if unwrapped_left_type == promoted_type { 'left' } else { 'right' } - pos := if unwrapped_left_type == promoted_type { - left_pos - } else { - right_pos - } - name := if unwrapped_left_type == promoted_type { - left_sym.name - } else { - right_sym.name - } - if node.op == .mod { - c.error('float modulo not allowed, use math.fmod() instead', pos) - } else { - c.error('${side} type of `${op_str}` cannot be non-integer type `${name}`', - pos) + mut promoted_type := ast.void_type + if node.op == .plus + && c.is_string_concat_pair(unaliased_left_type, unalias_right_type) { + promoted_type = ast.string_type + } else { + promoted_type = c.promote_keeping_aliases(unaliased_left_type, + unalias_right_type, left_sym.kind, right_sym.kind) + promoted_type = c.adjust_infix_int_literal_promotion(node.left, node.right, + unaliased_left_type, unalias_right_type, promoted_type) + // subtract pointers is allowed in unsafe block + is_allowed_pointer_arithmetic := left_type.is_any_kind_of_pointer() + && right_type.is_any_kind_of_pointer() && node.op == .minus + if is_allowed_pointer_arithmetic { + promoted_type = ast.int_type + } + if promoted_type.idx() == ast.void_type_idx { + left_name := c.table.type_to_str(unwrapped_left_type) + right_name := c.table.type_to_str(unwrapped_right_type) + c.error('mismatched types `${left_name}` and `${right_name}`', + left_right_pos) + } else if promoted_type.has_option_or_result() { + s := c.table.type_to_str(promoted_type) + c.error('`${node.op}` cannot be used with `${s}`', node.pos) + } else if promoted_type.is_float() { + if node.op in [.mod, .xor, .amp, .pipe] { + side := if unwrapped_left_type == promoted_type { + 'left' + } else { + 'right' + } + pos := if unwrapped_left_type == promoted_type { + left_pos + } else { + right_pos + } + name := if unwrapped_left_type == promoted_type { + left_sym.name + } else { + right_sym.name + } + if node.op == .mod { + c.error('float modulo not allowed, use math.fmod() instead', pos) + } else { + c.error('${side} type of `${op_str}` cannot be non-integer type `${name}`', + pos) + } } } - } - if node.op in [.div, .mod] { - c.check_div_mod_by_zero(node.right, node.op) + if node.op in [.div, .mod] { + c.check_div_mod_by_zero(node.right, node.op) + } } left_sym = c.table.sym(unwrapped_left_type) @@ -1122,8 +1153,9 @@ fn (mut c Checker) infix_expr(mut node ast.InfixExpr) ast.Type { right_type = c.unwrap_generic(right_type) right_sym = c.table.sym(right_type) } - types_match := c.symmetric_check(left_type, right_type) - && c.symmetric_check(right_type, left_type) + is_string_concat := node.op == .plus && c.is_string_concat_pair(left_type, right_type) + types_match := is_string_concat || (c.symmetric_check(left_type, right_type) + && c.symmetric_check(right_type, left_type)) left_allows_auto_deref := infix_expr_allows_auto_deref(node.left) right_allows_auto_deref := infix_expr_allows_auto_deref(node.right) unalias_left_type := c.table.unaliased_type(left_type) @@ -1220,8 +1252,8 @@ fn (mut c Checker) infix_expr(mut node ast.InfixExpr) ast.Type { } } } - if node.op == .plus && c.pref.warn_about_allocs && left_type == ast.string_type_idx - && right_type == ast.string_type_idx { + if node.op == .plus && c.pref.warn_about_allocs + && c.is_string_concat_pair(left_type, right_type) { c.warn_alloc('string concatenation', node.pos) } /* diff --git a/vlib/v/gen/c/assign.v b/vlib/v/gen/c/assign.v index 663c9a461..2447f151e 100644 --- a/vlib/v/gen/c/assign.v +++ b/vlib/v/gen/c/assign.v @@ -1600,11 +1600,19 @@ fn (mut g Gen) assign_stmt(node_ ast.AssignStmt) { && left.is_auto_deref_arg() { is_mut_arg_pointer_rebind = true } - if node.op == .plus_assign && unaliased_right_sym.kind == .string { + if node.op == .plus_assign && g.is_string_type(var_type) + && g.is_string_concat_type(val_type) { if g.is_autofree && !g.is_builtin_mod && !g.is_autofree_tmp - && val !in [ast.Ident, ast.StringLiteral, ast.SelectorExpr, ast.ComptimeSelector] { + && (!g.is_string_type(val_type) + || val !in [ast.Ident, ast.StringLiteral, ast.SelectorExpr, ast.ComptimeSelector]) { str_add_rhs_tmp = '_str_add_rhs_${node.pos.pos}_${i}' - g.writeln(g.autofree_tmp_arg_init_stmt('string ${str_add_rhs_tmp} = ', val)) + if g.is_string_type(val_type) { + g.writeln(g.autofree_tmp_arg_init_stmt('string ${str_add_rhs_tmp} = ', val)) + } else { + g.writeln('string ${str_add_rhs_tmp} = ${g.expr_string_with_cast(val, + val_type, ast.string_type)};') + val_type = ast.string_type + } val = ast.Expr(ast.Ident{ mod: g.cur_mod.name name: str_add_rhs_tmp diff --git a/vlib/v/gen/c/cgen.v b/vlib/v/gen/c/cgen.v index 5552bf90b..647ff8dfa 100644 --- a/vlib/v/gen/c/cgen.v +++ b/vlib/v/gen/c/cgen.v @@ -4519,6 +4519,8 @@ fn (mut g Gen) expr_with_cast(expr ast.Expr, got_type_raw ast.Type, expected_typ got_sym := g.table.sym(got_type) expected_is_ptr := expected_type.is_ptr() got_is_ptr := got_type.is_ptr() + unaliased_expected_type := g.table.unaliased_type(g.unwrap_generic(expected_type)).clear_flags() + unaliased_got_type := g.table.unaliased_type(g.unwrap_generic(got_type)).clear_flags() if g.can_convert_array_to_interface_array(got_type, expected_type) { fn_name := g.register_array_interface_cast_fn(got_type, expected_type) g.write('${fn_name}(') @@ -4526,6 +4528,23 @@ fn (mut g Gen) expr_with_cast(expr ast.Expr, got_type_raw ast.Type, expected_typ g.write(')') return } + if unaliased_expected_type == ast.string_type { + match unaliased_got_type { + ast.char_type { + g.write('builtin__u8_ascii_str((u8)(') + g.expr(expr) + g.write('))') + return + } + ast.rune_type { + g.write('builtin__rune_str((rune)(') + g.expr(expr) + g.write('))') + return + } + else {} + } + } // allow using the new Error struct as a string, to avoid a breaking change // TODO: temporary to allow people to migrate their code; remove soon if got_type == ast.error_type_idx && expected_type == ast.string_type_idx { diff --git a/vlib/v/gen/c/infix.v b/vlib/v/gen/c/infix.v index f224403b1..28b8f2b5a 100644 --- a/vlib/v/gen/c/infix.v +++ b/vlib/v/gen/c/infix.v @@ -1232,13 +1232,22 @@ fn (mut g Gen) is_string_type(typ ast.Type) bool { return g.unwrap(typ).unaliased_sym.kind == .string } +fn (mut g Gen) is_char_or_rune_string_concat_type(typ ast.Type) bool { + return g.table.unaliased_type(g.unwrap_generic(typ)).clear_flags() in [ast.char_type, ast.rune_type] +} + +fn (mut g Gen) is_string_concat_type(typ ast.Type) bool { + return g.is_string_type(typ) || g.is_char_or_rune_string_concat_type(typ) +} + fn (mut g Gen) is_string_concat_infix(node ast.InfixExpr) bool { if node.op != .plus { return false } left_type := g.type_resolver.get_type_or_default(node.left, node.left_type) right_type := g.type_resolver.get_type_or_default(node.right, node.right_type) - return g.is_string_type(left_type) && g.is_string_type(right_type) + return g.is_string_concat_type(left_type) && g.is_string_concat_type(right_type) + && (g.is_string_type(left_type) || g.is_string_type(right_type)) } fn (mut g Gen) collect_string_concat_parts(expr ast.Expr, mut parts []ast.Expr) { @@ -1266,7 +1275,15 @@ fn (mut g Gen) gen_string_concat_many(node ast.InfixExpr) bool { } mut parts := []ast.Expr{} g.collect_string_concat_parts(ast.Expr(node), mut parts) - if parts.len < 3 { + mut needs_plus_many := parts.len >= 3 + for part in parts { + part_type := g.type_resolver.get_type_or_default(part, part.type()) + if !g.is_string_type(part_type) { + needs_plus_many = true + break + } + } + if !needs_plus_many { return false } g.write('builtin__string_plus_many(${parts.len}, _MOV((string[${parts.len}]){') diff --git a/vlib/v/gen/c/testdata/string_concat_char_rune.out b/vlib/v/gen/c/testdata/string_concat_char_rune.out new file mode 100644 index 000000000..c83e36305 --- /dev/null +++ b/vlib/v/gen/c/testdata/string_concat_char_rune.out @@ -0,0 +1,6 @@ +hello world +AB +AAAAAA +AAAAAA +!abc +abc! diff --git a/vlib/v/gen/c/testdata/string_concat_char_rune.vv b/vlib/v/gen/c/testdata/string_concat_char_rune.vv new file mode 100644 index 000000000..f7f640ec8 --- /dev/null +++ b/vlib/v/gen/c/testdata/string_concat_char_rune.vv @@ -0,0 +1,16 @@ +fn main() { + mut s := 'hello' + s += ` ` + s += 'world' + println(s) + + mut ascii := '' + ascii += char(0x41) + ascii += `B` + println(ascii) + + println(char(0x41) + 'AAAAA') + println('AAAAA' + char(0x41)) + println(`!` + 'abc') + println('abc' + `!`) +} -- 2.39.5