From 927a1e258e7706e68b54de201e5d2dc6182e545d Mon Sep 17 00:00:00 2001 From: GGRei Date: Sun, 14 Jun 2026 23:48:37 +0200 Subject: [PATCH] v: preserve explicit generic string interpolation formats (#27455) --- vlib/v/ast/ast.v | 3 +- vlib/v/checker/str.v | 4 +- vlib/v/gen/c/str_intp.v | 14 +++---- vlib/v/parser/parser.v | 3 +- .../v/tests/generics/generics_str_intp_test.v | 39 +++++++++++++++++++ 5 files changed, 53 insertions(+), 10 deletions(-) diff --git a/vlib/v/ast/ast.v b/vlib/v/ast/ast.v index cb12931ed..5e29d9d1c 100644 --- a/vlib/v/ast/ast.v +++ b/vlib/v/ast/ast.v @@ -276,7 +276,8 @@ pub mut: fwidth_exprs []Expr precision_exprs []Expr fmts []u8 - need_fmts []bool // an explicit non-default fmt required, e.g. `x` + has_fmts []bool // a fixed format was explicitly written in source + need_fmts []bool // a non-default fmt is required, e.g. `x` } pub struct CharLiteral { diff --git a/vlib/v/checker/str.v b/vlib/v/checker/str.v index b5644cdba..5e12d5ec2 100644 --- a/vlib/v/checker/str.v +++ b/vlib/v/checker/str.v @@ -116,9 +116,11 @@ fn (mut c Checker) string_inter_lit(mut node ast.StringInterLiteral) ast.Type { ftyp } mut fmt := node.fmts[i] + has_explicit_fmt := i < node.has_fmts.len && node.has_fmts[i] // During generic recheck, reset auto-determined format specifiers // since the type may have changed between instantiations - if c.table.cur_concrete_types.len > 0 && !node.need_fmts[i] && fmt != `_` { + if c.table.cur_concrete_types.len > 0 && !has_explicit_fmt && !node.need_fmts[i] + && fmt != `_` { fmt = `_` } // analyze and validate format specifier diff --git a/vlib/v/gen/c/str_intp.v b/vlib/v/gen/c/str_intp.v index 0785649d1..ede1b2f5a 100644 --- a/vlib/v/gen/c/str_intp.v +++ b/vlib/v/gen/c/str_intp.v @@ -597,8 +597,9 @@ fn (mut g Gen) string_inter_literal(node ast.StringInterLiteral) { mut node_ := unsafe { node } mut fmts := node_.fmts.clone() for i, mut expr in node_.exprs { - if g.cur_fn != unsafe { nil } && g.cur_concrete_types.len > 0 && !node_.need_fmts[i] - && fmts[i] != `_` { + has_explicit_fmt := i < node_.has_fmts.len && node_.has_fmts[i] + if g.cur_fn != unsafe { nil } && g.cur_concrete_types.len > 0 && !has_explicit_fmt + && !node_.need_fmts[i] && fmts[i] != `_` { fmts[i] = `_` } mut resolved_if_guard_typ := ast.Type(0) @@ -667,8 +668,7 @@ fn (mut g Gen) string_inter_literal(node ast.StringInterLiteral) { ctyp = ctyp.clear_flag(.option) } node_.expr_types[i] = ctyp - if node_.fmts[i] == `_` - || (g.cur_fn != unsafe { nil } && g.cur_concrete_types.len > 0) { + if !has_explicit_fmt && fmts[i] == `_` { ftyp_sym := g.table.sym(ctyp) typ := if ftyp_sym.kind == .alias && !ftyp_sym.has_method('str') { g.table.unalias_num_type(ctyp) @@ -702,7 +702,7 @@ fn (mut g Gen) string_inter_literal(node ast.StringInterLiteral) { node_.expr_types[i] = field_typ } // Update format specifier if it was auto-determined and the type changed - if !node_.need_fmts[i] && fmts[i] == `_` { + if !has_explicit_fmt && !node_.need_fmts[i] && fmts[i] == `_` { ftyp_sym := g.table.sym(field_typ) new_typ := if ftyp_sym.kind == .alias && !ftyp_sym.has_method('str') { g.table.unalias_num_type(field_typ) @@ -722,7 +722,7 @@ fn (mut g Gen) string_inter_literal(node ast.StringInterLiteral) { && g.table.final_sym(field_typ).kind != .interface { field_typ = field_typ.deref() node_.expr_types[i] = field_typ - if !node_.need_fmts[i] { + if !has_explicit_fmt && !node_.need_fmts[i] { fmts[i] = g.get_default_fmt(field_typ, field_typ) } } @@ -735,7 +735,7 @@ fn (mut g Gen) string_inter_literal(node ast.StringInterLiteral) { // instead of falling through to `%p`. The pointer type is // preserved in expr_types so `str_val` still emits the // required `*` when reading the value. - if !node_.need_fmts[i] { + if !has_explicit_fmt && !node_.need_fmts[i] { deref_typ := field_typ.deref() ftyp_sym := g.table.sym(deref_typ) new_typ := if ftyp_sym.kind == .alias && !ftyp_sym.has_method('str') { diff --git a/vlib/v/parser/parser.v b/vlib/v/parser/parser.v index c1a6c23bf..3ad30a61c 100644 --- a/vlib/v/parser/parser.v +++ b/vlib/v/parser/parser.v @@ -2695,7 +2695,7 @@ fn (mut p Parser) string_expr() ast.Expr { } fwidths << fwidth fwidth_exprs << fwidth_expr - has_fmts << has_fmt + has_fmts << (has_fmt && fmt != `_`) precisions << precision precision_exprs << precision_expr visible_pluss << visible_plus @@ -2707,6 +2707,7 @@ fn (mut p Parser) string_expr() ast.Expr { node = ast.StringInterLiteral{ vals: vals exprs: exprs + has_fmts: has_fmts.clone() need_fmts: has_fmts fwidths: fwidths fwidth_exprs: fwidth_exprs diff --git a/vlib/v/tests/generics/generics_str_intp_test.v b/vlib/v/tests/generics/generics_str_intp_test.v index 856da3f93..3312b1021 100644 --- a/vlib/v/tests/generics/generics_str_intp_test.v +++ b/vlib/v/tests/generics/generics_str_intp_test.v @@ -25,3 +25,42 @@ fn test_generics_str_intp() { assert x.indent_str() == '100' assert y.indent_str() == 'hello' } + +struct Foo[T] { + bar T +} + +fn (b Foo[T]) str() string { + return '${b.bar:b}/${b.bar:o}/${b.bar:d}/${b.bar:x}' +} + +fn generic_int_format[T](value T) string { + return '${value:b}/${value:o}/${value:d}/${value:x}' +} + +fn generic_int_padded_format[T](value T) string { + return '${value:08x}/${value:+d}' +} + +// vfmt off +fn generic_default_format[T](value T) string { + return '${value:_}' +} +// vfmt on + +fn test_generic_string_interpolation_explicit_int_formats() { + assert generic_int_format[int](42) == '101010/52/42/2a' + assert generic_int_padded_format[int](42) == '0000002a/+42' + assert generic_default_format[int](42) == '42' + assert generic_default_format[string]('ok') == 'ok' + + baz := Foo[int]{ + bar: 42 + } + assert baz.str() == '101010/52/42/2a' + + baz_inferred := Foo{ + bar: 56 + } + assert baz_inferred.str() == '111000/70/56/38' +} -- 2.39.5