diff --git a/doc/docs.md b/doc/docs.md index 083e48cb6e..2e1f567cef 100644 --- a/doc/docs.md +++ b/doc/docs.md @@ -215,6 +215,14 @@ Both single and double quotes can be used to denote strings. For consistency, Interpolation syntax is pretty simple. It also works with fields: `'age = $user.age'`. If you need more complex expressions, use `${}`: `'can register = ${user.age > 13}'`. +Format specifiers similar to those in C's `printf()` are supported, too. `f`, `g`, `x`, etc. are optional +and specify the output format. The compiler takes care of the storage size, so there is no `hd` or `llu`. + +```v +println('x = ${x:12.3f}'` +println('${item:-20} ${n:20d}) +``` + All operators in V must have values of the same type on both sides. This code will not compile if `age` is an `int`: ```v diff --git a/vlib/benchmark/benchmark.v b/vlib/benchmark/benchmark.v index 1776c75ae5..112db9d7fb 100644 --- a/vlib/benchmark/benchmark.v +++ b/vlib/benchmark/benchmark.v @@ -218,7 +218,7 @@ pub fn (b &Benchmark) total_duration() i64 { // ////////////////////////////////////////////////////////////////// fn (b &Benchmark) tdiff_in_ms(s string, tdiff i64) string { if b.verbose { - return '${tdiff/1000.0:9.3f} ms $s' + return '${f64(tdiff)/1000.0:9.3f} ms $s' } return s } diff --git a/vlib/builtin/utf8.v b/vlib/builtin/utf8.v index 9bb5dd7e39..dbf6fb349b 100644 --- a/vlib/builtin/utf8.v +++ b/vlib/builtin/utf8.v @@ -174,6 +174,21 @@ fn utf8_len(c byte) int { return b } +// Calculate string length for formatting, i.e. number of "characters" +fn utf8_str_len(s string) int { + mut l := 0 + for i := 0; i < s.len; i++ { + l++ + c := s.str[i] + if (c & (1 << 7)) != 0 { + for t := byte(1 << 6); (c & t) != 0; t >>= 1 { + i++ + } + } + } + return l +} + // Reads an utf8 character from standard input pub fn utf8_getchar() int { c := C.getchar() diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index b900ec5b78..b085f5e279 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -2023,13 +2023,13 @@ pub fn (mut c Checker) map_init(node mut ast.MapInit) table.Type { if !c.table.check(key_type, key0_type) { key0_type_sym := c.table.get_type_symbol(key0_type) key_type_sym := c.table.get_type_symbol(key_type) - c.error('map init: cannot use `$key_type_sym.name` as `$key0_type_sym` for map key', + c.error('map init: cannot use `$key_type_sym.name` as `$key0_type_sym.name` for map key', node.pos) } if !c.table.check(val_type, val0_type) { val0_type_sym := c.table.get_type_symbol(val0_type) val_type_sym := c.table.get_type_symbol(val_type) - c.error('map init: cannot use `$val_type_sym.name` as `$val0_type_sym` for map value', + c.error('map init: cannot use `$val_type_sym.name` as `$val0_type_sym.name` for map value', node.pos) } } diff --git a/vlib/v/gen/cgen.v b/vlib/v/gen/cgen.v index b9d2cf6bb4..17dc2c7133 100644 --- a/vlib/v/gen/cgen.v +++ b/vlib/v/gen/cgen.v @@ -4,6 +4,7 @@ module gen import strings +import strconv import v.ast import v.table import v.pref @@ -211,7 +212,8 @@ pub fn (mut g Gen) init() { g.cheaders.writeln('#include ') // int64_t etc g.cheaders.writeln(c_builtin_types) g.cheaders.writeln(c_headers) - g.definitions.writeln('\nstring _STR(const char*, ...);\n') + g.definitions.writeln('\nvoid _STR_PRINT_ARG(const char*, char**, int*, int*, int, ...);\n') + g.definitions.writeln('\nstring _STR(const char*, int, ...);\n') g.definitions.writeln('\nstring _STR_TMP(const char*, ...);\n') g.write_builtin_types() g.write_typedef_types() @@ -2173,15 +2175,76 @@ fn (mut g Gen) write_init_function() { fn (mut g Gen) write_str_fn_definitions() { // _STR function can't be defined in vlib g.writeln(' -string _STR(const char *fmt, ...) { +void _STR_PRINT_ARG(const char *fmt, char** refbufp, int *nbytes, int *memsize, int guess, ...) { + va_list args; + va_start(args, guess); + for(;;) { + if (guess < *memsize - *nbytes) { + guess = vsnprintf(*refbufp + *nbytes, *memsize - *nbytes, fmt, args); + if (guess < *memsize - *nbytes) { // result did fit into buffer + *nbytes += guess; + return; + } + } + // increase buffer (somewhat exponentially) + *memsize += (*memsize + *memsize) / 3 + guess; + *refbufp = realloc(*refbufp, *memsize); + } +} + +string _STR(const char *fmt, int nfmts, ...) { va_list argptr; - va_start(argptr, fmt); - size_t len = vsnprintf(0, 0, fmt, argptr) + 1; - va_end(argptr); - byte* buf = malloc(len); - va_start(argptr, fmt); - vsprintf((char *)buf, fmt, argptr); + int memsize = 128; + int nbytes = 0; + char* buf = malloc(memsize); + va_start(argptr, nfmts); + for (int i=0; i= '+"'E'"+' && fup <= '+"'G'"+') { // floating point + _STR_PRINT_ARG(fmt, &buf, &nbytes, &memsize, k+10, va_arg(argptr, double)); + } else if (f == '+"'s'"+') { // v string + string s = va_arg(argptr, string); + if (fmt[k-4] == '+"'*'"+') { // %*.*s + int fwidth = va_arg(argptr, int); + if (fwidth < 0) + fwidth -= (s.len - utf8_str_len(s)); + else + fwidth += (s.len - utf8_str_len(s)); + _STR_PRINT_ARG(fmt, &buf, &nbytes, &memsize, k+fwidth-4, fwidth, s.len, s.str); + } else { // %.*s + _STR_PRINT_ARG(fmt, &buf, &nbytes, &memsize, k+s.len-4, s.len, s.str); + } + } else { + v_panic(tos3("Invaid format specifier")); + } + } else { + if (k) + _STR_PRINT_ARG(fmt, &buf, &nbytes, &memsize, k); + } + fmt += k+1; + } va_end(argptr); + buf[nbytes] = 0; + buf = realloc(buf, nbytes+1); #ifdef DEBUG_ALLOC puts("_STR:"); puts(buf); @@ -2362,89 +2425,128 @@ fn (g Gen) sort_structs(typesa []table.TypeSymbol) []table.TypeSymbol { fn (mut g Gen) string_inter_literal(node ast.StringInterLiteral) { g.write('_STR("') // Build the string with % + mut fieldwidths := []int{} + mut specs := []byte{} + mut num_fmts := 1 for i, val in node.vals { escaped_val := val.replace_each(['"', '\\"', '\r\n', '\\n', '\n', '\\n', '%', '%%']) g.write(escaped_val) if i >= node.exprs.len { + fieldwidths << 0 + specs << `_` continue } - // TODO: fix match, sum type false positive - // match node.expr_types[i] { - // table.string_type { - // g.write('%.*s') - // } - // table.int_type { - // g.write('%d') - // } - // else {} - // } + num_fmts++ sym := g.table.get_type_symbol(node.expr_types[i]) sfmt := node.expr_fmts[i] + mut fspec := `_` // placeholder + mut fmt := '' // field width and precision if sfmt.len > 0 { - fspec := sfmt[sfmt.len - 1] - if fspec == `s` && node.expr_types[i] != table.string_type { - verror('only V strings can be formatted with a ${sfmt} format') + // analyze and validate format specifier + if sfmt[sfmt.len - 1] in [`E`, `F`, `G`, `e`, `f`, `g`, `e`, + `d`, `u`, `x`, `X`, `o`, `c`, `s`] { + fspec = sfmt[sfmt.len - 1] + } + fmt = if fspec == `_` { + sfmt[1..sfmt.len] + } else { + sfmt[1..sfmt.len - 1] } - g.write('%' + sfmt[1..]) - } else if node.expr_types[i] in [table.string_type, table.bool_type] || sym.kind in - [.enum_, .array, .array_fixed] { - g.write('%.*s') - } else if node.expr_types[i] in [table.f32_type, table.f64_type] { - g.write('%g') - } else if sym.kind in [.struct_, .map] && !sym.has_method('str') { - g.write('%.*s') - } else if node.expr_types[i] == table.i16_type { - g.write('%"PRId16"') - } else if node.expr_types[i] == table.u16_type { - g.write('%"PRIu16"') - } else if node.expr_types[i] == table.u32_type { - g.write('%"PRIu32"') - } else if node.expr_types[i] == table.i64_type { - g.write('%"PRId64"') - } else if node.expr_types[i] == table.u64_type { - g.write('%"PRIu64"') - } else if g.typ(node.expr_types[i]).starts_with('Option') { - g.write('%.*s') - } else { - g.write('%"PRId32"') } + if fspec == `_` { // set default representation for type if still missing + if node.expr_types[i].is_float() { + fspec = `g` + } else if node.expr_types[i].is_signed() { + fspec = `d` + } else if node.expr_types[i].is_unsigned() { + fspec = `u` + } else if node.expr_types[i] in [table.string_type, table.bool_type] || + sym.kind in [.enum_, .array, .array_fixed, .struct_, .map] || + g.typ(node.expr_types[i]).starts_with('Option') || + sym.has_method('str') { + fspec = `s` + } else { + // default to int - TODO: should better be checked + fspec = `d` + } + } + fields := fmt.split('.') + // validate format + // only floats should have precision specifier + if fields.len > 2 || fields.len == 2 && !(node.expr_types[i].is_float()) || + node.expr_types[i].is_signed() && !(fspec in [`d`, `c`, `x`, `X`, `o`]) || + node.expr_types[i].is_unsigned() && !(fspec in [`u`, `x`, `X`, `o`, `c`]) || + node.expr_types[i].is_float() && !(fspec in [`E`, `F`, `G`, `e`, `f`, `g`, `e`]) { + verror('illegal format specifier ${fspec:c} for type ${g.table.get_type_name(node.expr_types[i])}') + } + // make sure that format paramters are valid numbers + for j, f in fields { + for k, c in f { + if (c < `0` || c > `9`) && + !(j == 0 && k == 0 && (node.expr_types[i].is_number() && c == `+` || c == `-`)) { + verror('illegal character ${c:c} in format specifier ${fmt}') + } + } + } + specs << fspec + fieldwidths << if fields.len == 0 { + 0 + } else { + strconv.atoi(fields[0]) + } + // write correct format specifier to intermediate string + g.write('%') + if fspec == `s` { + if fields.len == 0 || strconv.atoi(fields[0]) == 0 { + g.write('.*s') + } else { + g.write('*.*s') + } + } else if node.expr_types[i].is_float() { + g.write('$fmt${fspec:c}') + } else if node.expr_types[i].is_int() { + if fspec == `c` { + if node.expr_types[i].idx() in [table.i64_type_idx table.f64_type_idx] { + verror("64 bit integer types cannot be interpolated as character") + } else { + g.write('${fmt}c') + } + } else { + g.write('${fmt}"PRI${fspec:c}') + if node.expr_types[i] in [table.i8_type, table.byte_type] { + g.write('8') + } else if node.expr_types[i] in [table.i16_type, table.u16_type] { + g.write('16') + } else if node.expr_types[i] in [table.i64_type, table.u64_type] { + g.write('64') + } else { + g.write('32') + } + g.write('"') + } + } else { + // TODO: better check this case + g.write('${fmt}"PRId32"') + } + g.write('\\000') } - g.write('", ') + g.write('", $num_fmts, ') // Build args for i, expr in node.exprs { - sfmt := node.expr_fmts[i] - if sfmt.len > 0 { - fspec := sfmt[sfmt.len - 1] - if fspec == `s` && node.expr_types[i] == table.string_type { - g.expr(expr) - g.write('.str') - } else { - g.expr(expr) - } - } else if node.expr_types[i] == table.string_type { - // `name.str, name.len,` + if node.expr_types[i] == table.string_type { g.expr(expr) - g.write('.len, ') - g.expr(expr) - g.write('.str') } else if node.expr_types[i] == table.bool_type { g.expr(expr) - g.write(' ? 4 : 5, ') + g.write(' ? _SLIT("true") : _SLIT("false")') + } else if node.expr_types[i].is_number() || specs[i] == `d` { g.expr(expr) - g.write(' ? "true" : "false"') - } else if node.expr_types[i] in [table.f32_type, table.f64_type] { - g.expr(expr) - } else { + } else if specs[i] == `s` { sym := g.table.get_type_symbol(node.expr_types[i]) if node.expr_types[i].flag_is(.variadic) { str_fn_name := g.gen_str_for_type(node.expr_types[i]) g.write('${str_fn_name}(') g.expr(expr) g.write(')') - g.write('.len, ') - g.write('${str_fn_name}(') - g.expr(expr) - g.write(').str') } else if sym.kind == .enum_ { is_var := match node.exprs[i] { ast.SelectorExpr { true } @@ -2456,58 +2558,51 @@ fn (mut g Gen) string_inter_literal(node ast.StringInterLiteral) { g.write('${str_fn_name}(') g.enum_expr(expr) g.write(')') - g.write('.len, ') - g.write('${str_fn_name}(') - g.enum_expr(expr) - g.write(').str') } else { g.write('tos3("') g.enum_expr(expr) g.write('")') - g.write('.len, ') - g.write('"') - g.enum_expr(expr) - g.write('"') } - } else if sym.kind in [.array, .array_fixed] { - str_fn_name := g.gen_str_for_type(node.expr_types[i]) - g.write('${str_fn_name}(') + } else if sym.has_method('str') || sym.kind in [.array, .array_fixed, .map, .struct_] { + is_p := node.expr_types[i].is_ptr() + val_type := if is_p { + node.expr_types[i].deref() + } else { + node.expr_types[i] + } + str_fn_name := g.gen_str_for_type(val_type) + if is_p { + g.write('string_add(_SLIT("&"), ${str_fn_name}(*(') + } else { + g.write('${str_fn_name}(') + } g.expr(expr) - g.write(')') - g.write('.len, ') - g.write('${str_fn_name}(') - g.expr(expr) - g.write(').str') - } else if sym.kind == .map && !sym.has_method('str') { - str_fn_name := g.gen_str_for_type(node.expr_types[i]) - g.write('${str_fn_name}(') - g.expr(expr) - g.write(')') - g.write('.len, ') - g.write('${str_fn_name}(') - g.expr(expr) - g.write(').str') - } else if sym.kind == .struct_ && !sym.has_method('str') { - str_fn_name := g.gen_str_for_type(node.expr_types[i]) - g.write('${str_fn_name}(') - g.expr(expr) - g.write(',0)') - g.write('.len, ') - g.write('${str_fn_name}(') - g.expr(expr) - g.write(',0).str') + if sym.kind == .struct_ && !sym.has_method('str') { + if is_p { + g.write('),0))') + } else { + g.write(',0)') + } + } else { + if is_p { + g.write(')))') + } else { + g.write(')') + } + } } else if g.typ(node.expr_types[i]).starts_with('Option') { str_fn_name := 'Option_str' g.write('${str_fn_name}(*(Option*)&') g.expr(expr) g.write(')') - g.write('.len, ') - g.write('${str_fn_name}(*(Option*)&') - g.expr(expr) - g.write(').str') } else { - g.expr(expr) + verror('cannot convert to string') } + } else { + g.expr(expr) + } + if specs[i] == `s` && fieldwidths[i] != 0 { + g.write(', ${fieldwidths[i]}') } if i < node.exprs.len - 1 { g.write(', ') @@ -3191,9 +3286,9 @@ fn (mut g Gen) gen_str_for_struct(info table.Struct, styp, str_fn_name string) { g.auto_str_funcs.writeln('\treturn _STR("${clean_struct_v_type_name} {\\n"') for field in info.fields { fmt := g.type_to_fmt(field.typ) - g.auto_str_funcs.writeln('\t\t"%.*s ' + '$field.name: $fmt\\n"') + g.auto_str_funcs.writeln('\t\t"%.*s\\000 ' + '$field.name: $fmt\\n"') } - g.auto_str_funcs.write('\t\t"%.*s}"') + g.auto_str_funcs.write('\t\t"%.*s\\000}", ${2*(info.fields.len+1)}') if info.fields.len > 0 { g.auto_str_funcs.write(',\n\t\t') for i, field in info.fields { @@ -3203,23 +3298,18 @@ fn (mut g Gen) gen_str_for_struct(info table.Struct, styp, str_fn_name string) { field_styp := g.typ(field.typ) field_styp_fn_name := if has_custom_str { '${field_styp}_str' } else { fnames2strfunc[field_styp] } if sym.kind == .enum_ { - g.auto_str_funcs.write('indents.len, indents.str, ') - g.auto_str_funcs.write('${field_styp_fn_name}( it->${field.name} ).len, ') - g.auto_str_funcs.write('${field_styp_fn_name}( it->${field.name} ).str ') + g.auto_str_funcs.write('indents, ') + g.auto_str_funcs.write('${field_styp_fn_name}( it->${field.name} ) ') } else if sym.kind == .struct_ { - g.auto_str_funcs.write('indents.len, indents.str, ') - g.auto_str_funcs.write('${field_styp_fn_name}( it->${field.name}${second_str_param} ).len, ') - g.auto_str_funcs.write('${field_styp_fn_name}( it->${field.name}${second_str_param} ).str ') + g.auto_str_funcs.write('indents, ') + g.auto_str_funcs.write('${field_styp_fn_name}( it->${field.name}${second_str_param} ) ') } else if sym.kind in [.array, .array_fixed] { - g.auto_str_funcs.write('indents.len, indents.str, ') - g.auto_str_funcs.write('${field_styp_fn_name}( it->${field.name}).len, ') - g.auto_str_funcs.write('${field_styp_fn_name}( it->${field.name}).str ') + g.auto_str_funcs.write('indents, ') + g.auto_str_funcs.write('${field_styp_fn_name}( it->${field.name}) ') } else { - g.auto_str_funcs.write('indents.len, indents.str, it->${field.name}') - if field.typ == table.string_type { - g.auto_str_funcs.write('.len, it->${field.name}.str') - } else if field.typ == table.bool_type { - g.auto_str_funcs.write(' ? 4 : 5, it->${field.name} ? "true" : "false"') + g.auto_str_funcs.write('indents, it->${field.name}') + if field.typ == table.bool_type { + g.auto_str_funcs.write(' ? _SLIT("true") : _SLIT("false")') } } if i < info.fields.len - 1 { @@ -3228,7 +3318,7 @@ fn (mut g Gen) gen_str_for_struct(info table.Struct, styp, str_fn_name string) { } } g.auto_str_funcs.writeln(',') - g.auto_str_funcs.writeln('\t\tindents.len, indents.str);') + g.auto_str_funcs.writeln('\t\tindents);') g.auto_str_funcs.writeln('}') } @@ -3247,7 +3337,7 @@ fn (mut g Gen) gen_str_for_array(info table.Array, styp, str_fn_name string) { if sym.kind == .struct_ && !sym.has_method('str') { g.auto_str_funcs.writeln('\t\tstrings__Builder_write(&sb, ${field_styp}_str(it,0));') } else if sym.kind in [.f32, .f64] { - g.auto_str_funcs.writeln('\t\tstrings__Builder_write(&sb, _STR("%g", it));') + g.auto_str_funcs.writeln('\t\tstrings__Builder_write(&sb, _STR("%g", 1, it));') } else { g.auto_str_funcs.writeln('\t\tstrings__Builder_write(&sb, ${field_styp}_str(it));') } @@ -3274,9 +3364,9 @@ fn (mut g Gen) gen_str_for_array_fixed(info table.ArrayFixed, styp, str_fn_name if sym.kind == .struct_ && !sym.has_method('str') { g.auto_str_funcs.writeln('\t\tstrings__Builder_write(&sb, ${field_styp}_str(a[i],0));') } else if sym.kind in [.f32, .f64] { - g.auto_str_funcs.writeln('\t\tstrings__Builder_write(&sb, _STR("%g", a[i]));') + g.auto_str_funcs.writeln('\t\tstrings__Builder_write(&sb, _STR("%g", 1, a[i]));') } else if sym.kind == .string { - g.auto_str_funcs.writeln('\t\tstrings__Builder_write(&sb, _STR("\'%.*s\'", a[i].len, a[i].str));') + g.auto_str_funcs.writeln('\t\tstrings__Builder_write(&sb, _STR("\'%.*s\\000\'", 2, a[i]));') } else { g.auto_str_funcs.writeln('\t\tstrings__Builder_write(&sb, ${field_styp}_str(a[i]));') } @@ -3307,18 +3397,18 @@ fn (mut g Gen) gen_str_for_map(info table.Map, styp, str_fn_name string) { g.auto_str_funcs.writeln('\tstrings__Builder_write(&sb, tos3("{"));') g.auto_str_funcs.writeln('\tfor (unsigned int i = 0; i < m.key_values.size; i++) {') g.auto_str_funcs.writeln('\t\tstring key = (*(string*)DenseArray_get(m.key_values, i));') - g.auto_str_funcs.writeln('\t\tstrings__Builder_write(&sb, _STR("\'%.*s\'", key.len, key.str));') + g.auto_str_funcs.writeln('\t\tstrings__Builder_write(&sb, _STR("\'%.*s\\000\'", 2, key));') g.auto_str_funcs.writeln('\t\tstrings__Builder_write(&sb, tos3(": "));') g.auto_str_funcs.write('\t$val_styp it = (*($val_styp*)map_get3(') g.auto_str_funcs.write('m, (*(string*)DenseArray_get(m.key_values, i))') g.auto_str_funcs.write(', ') g.auto_str_funcs.writeln(' &($val_styp[]) { $zero }));') if val_sym.kind == .string { - g.auto_str_funcs.writeln('\t\tstrings__Builder_write(&sb, _STR("\'%.*s\'", it.len, it.str));') + g.auto_str_funcs.writeln('\t\tstrings__Builder_write(&sb, _STR("\'%.*s\\000\'", 2, it));') } else if val_sym.kind == .struct_ && !val_sym.has_method('str') { g.auto_str_funcs.writeln('\t\tstrings__Builder_write(&sb, ${val_styp}_str(it,0));') } else if val_sym.kind in [.f32, .f64] { - g.auto_str_funcs.writeln('\t\tstrings__Builder_write(&sb, _STR("%g", it));') + g.auto_str_funcs.writeln('\t\tstrings__Builder_write(&sb, _STR("%g", 1, it));') } else { g.auto_str_funcs.writeln('\t\tstrings__Builder_write(&sb, ${val_styp}_str(it));') } @@ -3350,17 +3440,17 @@ fn (mut g Gen) gen_str_for_varg(styp, str_fn_name string) { fn (g Gen) type_to_fmt(typ table.Type) string { sym := g.table.get_type_symbol(typ) if sym.kind in [.struct_, .array, .array_fixed, .map] { - return '%.*s' + return '%.*s\\000' } else if typ == table.string_type { - return "\'%.*s\'" + return "\'%.*s\\000\'" } else if typ == table.bool_type { - return '%.*s' + return '%.*s\\000' } else if sym.kind == .enum_ { - return '%.*s' + return '%.*s\\000' } else if typ in [table.f32_type, table.f64_type] { - return '%g' // g removes trailing zeros unlike %f + return '%g\\000' // g removes trailing zeros unlike %f } - return '%d' + return '%d\\000' } // Generates interface table and interface indexes diff --git a/vlib/v/table/atypes.v b/vlib/v/table/atypes.v index 32d738587a..3285ce2e61 100644 --- a/vlib/v/table/atypes.v +++ b/vlib/v/table/atypes.v @@ -140,6 +140,16 @@ pub fn (typ Type) is_int() bool { return typ.idx() in integer_type_idxs } +[inline] +pub fn (typ Type) is_signed() bool { + return typ.idx() in signed_integer_type_idxs +} + +[inline] +pub fn (typ Type) is_unsigned() bool { + return typ.idx() in unsigned_integer_type_idxs +} + [inline] pub fn (typ Type) is_number() bool { return typ.idx() in number_type_idxs @@ -175,6 +185,8 @@ pub const ( u32_type_idx, u64_type_idx ] + signed_integer_type_idxs = [i8_type_idx, i16_type_idx, int_type_idx, i64_type_idx] + unsigned_integer_type_idxs = [byte_type_idx, u16_type_idx, u32_type_idx, u64_type_idx] float_type_idxs = [f32_type_idx, f64_type_idx] number_type_idxs = [i8_type_idx, i16_type_idx, int_type_idx, i64_type_idx, byte_type_idx, u16_type_idx, diff --git a/vlib/v/tests/string_interpolation_test.v b/vlib/v/tests/string_interpolation_test.v index 6f07c9eada..3819381219 100644 --- a/vlib/v/tests/string_interpolation_test.v +++ b/vlib/v/tests/string_interpolation_test.v @@ -86,4 +86,31 @@ fn test_inttypes_string_interpolation() { assert '$s $us' == '-23456 54321' assert '$ui $i' == '3421958087 -1622999040' assert '$l $ul' == '-7694555558525237396 17234006112912956370' + assert '>${s:11}< >${us:-13}<-' == '> -23456< >54321 <-' + assert '0x${ul:-19x}< >${l:22d}<-' == '0xef2b7d4001165bd2 < > -7694555558525237396<-' +} + +fn test_utf8_string_interpolation() { + a := 'à-côté' + st := 'Sträßle' + m := '10€' + assert '$a $st $m' == 'à-côté Sträßle 10€' + assert '>${a:10}< >${st:-8}< >${m:5}<-' == '> à-côté< >Sträßle < > 10€<-' + e := '\u20AC' // Eurosign + // TODO: this fails with MSVC and tcc + // assert '100.00 $e' == '100.00 €' +} + +struct S { + v1 int + v2 f64 +} + +fn (s S) str() string { + return '[${s.v1}, ${s.v2:.3f}]' +} + +fn test_string_interpolation_str_evaluation() { + mut x := S{17, 13.455893} + assert '$x' == '[17, 13.456]' }