diff --git a/vlib/v/ast/ast.v b/vlib/v/ast/ast.v index 3de89ccae8..0fe52b3cd0 100644 --- a/vlib/v/ast/ast.v +++ b/vlib/v/ast/ast.v @@ -73,10 +73,16 @@ pub struct StringInterLiteral { pub: vals []string exprs []Expr - expr_fmts []string + fwidths []int + precisions []int + pluss []bool + fills []bool + fmt_poss []token.Position pos token.Position pub mut: expr_types []table.Type + fmts []byte + need_fmts []bool // an explicit non-default fmt required, e.g. `x` } pub struct CharLiteral { diff --git a/vlib/v/ast/str.v b/vlib/v/ast/str.v index 85b5c14d93..a61e5d4661 100644 --- a/vlib/v/ast/str.v +++ b/vlib/v/ast/str.v @@ -160,10 +160,28 @@ pub fn (x Expr) str() string { continue } res << '$' - if it.expr_fmts[i].len > 0 { + needs_fspec := it.need_fmts[i] || it.pluss[i] || (it.fills[i] && it.fwidths[i] >= 0) || it.fwidths[i] != 0 || it.precisions[i] != 0 + if needs_fspec || (it.exprs[i] !is Ident && it.exprs[i] !is SelectorExpr) { res << '{' res << it.exprs[i].str() - res << it.expr_fmts[i] + if needs_fspec { + res << ':' + if it.pluss[i] { + res << '+' + } + if it.fills[i] && it.fwidths[i] >= 0 { + res << '0' + } + if it.fwidths[i] != 0 { + res << '${it.fwidths[i]}' + } + if it.precisions[i] != 0 { + res << '.${it.precisions[i]}' + } + if it.need_fmts[i] { + res << '${it.fmts[i]:c}' + } + } res << '}' } else { res << it.exprs[i].str() diff --git a/vlib/v/checker/check_types.v b/vlib/v/checker/check_types.v index 2525944ca1..993e6273f8 100644 --- a/vlib/v/checker/check_types.v +++ b/vlib/v/checker/check_types.v @@ -5,6 +5,7 @@ module checker import v.table import v.token +import v.ast pub fn (c &Checker) check_basic(got, expected table.Type) bool { t := c.table @@ -254,3 +255,66 @@ pub fn (c &Checker) symmetric_check(left, right table.Type) bool { } return c.check_basic(left, right) } + +pub fn (c &Checker) get_default_fmt(ftyp, typ table.Type) byte { + if typ.is_float() { + return `g` + } else if typ.is_signed() || typ.is_any_int() { + return `d` + } else if typ.is_unsigned() { + return `u` + } else if typ.is_pointer() { + return `p` + } else { + sym := c.table.get_type_symbol(ftyp) + if ftyp in [table.string_type, table.bool_type] || sym.kind in + [.enum_, .array, .array_fixed, .struct_, .map] || ftyp.has_flag(.optional) || + sym.has_method('str') { + return `s` + } else { + return `_` + } + } +} + +pub fn (c &Checker) string_inter_lit(mut node ast.StringInterLiteral) table.Type { + for i, expr in node.exprs { + ftyp := c.expr(expr) + node.expr_types << ftyp + typ := c.table.unalias_num_type(ftyp) + mut fmt := node.fmts[i] + // analyze and validate format specifier + if fmt !in [`E`, `F`, `G`, `e`, `f`, `g`, `d`, `u`, `x`, `X`, `o`, `c`, `s`, `p`, `_`] { + c.error('unknown format specifier `${fmt:c}`', node.fmt_poss[i]) + } + if fmt == `_` { // set default representation for type if none has been given + fmt = c.get_default_fmt(ftyp, typ) + if fmt == `_` { + c.error('no known default format for type `${c.table.get_type_name(ftyp)}`', + node.fmt_poss[i]) + } else { + node.fmts[i] = fmt + node.need_fmts[i] = false + } + } else { // check if given format specifier is valid for type + if node.precisions[i] != 0 && !typ.is_float() { + c.error('precision specification only valid for float types', node.fmt_poss[i]) + } + if node.pluss[i] && !typ.is_number() { + c.error('plus prefix only allowd for numbers', node.fmt_poss[i]) + } + if (typ.is_unsigned() && fmt !in [`u`, `x`, `X`, `o`, `c`]) || + (typ.is_signed() && fmt !in [`d`, `x`, `X`, `o`, `c`]) || + (typ.is_any_int() && fmt !in [`d`, `c`, `x`, `X`, `o`, `u`, `x`, `X`, `o`]) || + (typ.is_float() && fmt !in [`E`, `F`, `G`, `e`, `f`, `g`]) || + (typ.is_pointer() && fmt !in [`p`, `x`, `X`]) || + (typ.is_string() && fmt != `s`) || + (typ.idx() in [table.i64_type_idx, table.f64_type_idx] && fmt == `c`) { + c.error('illegal format specifier `${fmt:c}` for type `${c.table.get_type_name(ftyp)}`', + node.fmt_poss[i]) + } + node.need_fmts[i] = fmt != c.get_default_fmt(ftyp, typ) + } + } + return table.string_type +} diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index 4db86be2e4..80f7391489 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -1985,10 +1985,7 @@ pub fn (mut c Checker) expr(node ast.Expr) table.Type { return table.string_type } ast.StringInterLiteral { - for expr in it.exprs { - it.expr_types << c.expr(expr) - } - return table.string_type + return c.string_inter_lit(mut it) } ast.StructInit { return c.struct_init(mut it) diff --git a/vlib/v/checker/tests/string_interpolation_invalid_fmt.out b/vlib/v/checker/tests/string_interpolation_invalid_fmt.out new file mode 100644 index 0000000000..8ffdc6638d --- /dev/null +++ b/vlib/v/checker/tests/string_interpolation_invalid_fmt.out @@ -0,0 +1,7 @@ +vlib/v/checker/tests/string_interpolation_invalid_fmt.v:3:12: error: format specifier may only be one letter + 1 | fn interpolate_wrong() string { + 2 | a := 5 + 3 | x := '${a:xy}' + | ~~ + 4 | return x + 5 | } diff --git a/vlib/v/checker/tests/string_interpolation_invalid_fmt.vv b/vlib/v/checker/tests/string_interpolation_invalid_fmt.vv new file mode 100644 index 0000000000..bfc8db4e04 --- /dev/null +++ b/vlib/v/checker/tests/string_interpolation_invalid_fmt.vv @@ -0,0 +1,5 @@ +fn interpolate_wrong() string { + a := 5 + x := '${a:xy}' + return x +} diff --git a/vlib/v/checker/tests/string_interpolation_wrong_fmt.out b/vlib/v/checker/tests/string_interpolation_wrong_fmt.out new file mode 100644 index 0000000000..b7dee9387a --- /dev/null +++ b/vlib/v/checker/tests/string_interpolation_wrong_fmt.out @@ -0,0 +1,63 @@ +vlib/v/checker/tests/string_interpolation_wrong_fmt.v:3:16: error: precision specification only valid for float types + 1 | fn interpolate_str() string { + 2 | a := 'hallo' + 3 | x := '>${a:8.3s}<' + | ^ + 4 | y := '${a:G}' + 5 | z := '${a:d}' +vlib/v/checker/tests/string_interpolation_wrong_fmt.v:4:12: error: illegal format specifier `G` for type `string` + 2 | a := 'hallo' + 3 | x := '>${a:8.3s}<' + 4 | y := '${a:G}' + | ^ + 5 | z := '${a:d}' + 6 | return x + y + z +vlib/v/checker/tests/string_interpolation_wrong_fmt.v:5:12: error: illegal format specifier `d` for type `string` + 3 | x := '>${a:8.3s}<' + 4 | y := '${a:G}' + 5 | z := '${a:d}' + | ^ + 6 | return x + y + z + 7 | } +vlib/v/checker/tests/string_interpolation_wrong_fmt.v:11:15: error: illegal format specifier `s` for type `f64` + 9 | fn interpolate_f64() string { + 10 | b := 1367.57 + 11 | x := '>${b:20s}<' + | ^ + 12 | y := '${b:d}' + 13 | return x + y +vlib/v/checker/tests/string_interpolation_wrong_fmt.v:12:12: error: illegal format specifier `d` for type `f64` + 10 | b := 1367.57 + 11 | x := '>${b:20s}<' + 12 | y := '${b:d}' + | ^ + 13 | return x + y + 14 | } +vlib/v/checker/tests/string_interpolation_wrong_fmt.v:19:14: error: illegal format specifier `d` for type `u32` + 17 | u := u32(15) + 18 | s := -12 + 19 | x := '${u:13d}' + | ^ + 20 | y := '${s:04u}' + 21 | z := '${s:f}' +vlib/v/checker/tests/string_interpolation_wrong_fmt.v:20:14: error: illegal format specifier `u` for type `int` + 18 | s := -12 + 19 | x := '${u:13d}' + 20 | y := '${s:04u}' + | ^ + 21 | z := '${s:f}' + 22 | q := '${u:v}' +vlib/v/checker/tests/string_interpolation_wrong_fmt.v:21:12: error: illegal format specifier `f` for type `int` + 19 | x := '${u:13d}' + 20 | y := '${s:04u}' + 21 | z := '${s:f}' + | ^ + 22 | q := '${u:v}' + 23 | return x + y + z + q +vlib/v/checker/tests/string_interpolation_wrong_fmt.v:22:12: error: unknown format specifier `v` + 20 | y := '${s:04u}' + 21 | z := '${s:f}' + 22 | q := '${u:v}' + | ^ + 23 | return x + y + z + q + 24 | } diff --git a/vlib/v/checker/tests/string_interpolation_wrong_fmt.vv b/vlib/v/checker/tests/string_interpolation_wrong_fmt.vv new file mode 100644 index 0000000000..38aa5f31a4 --- /dev/null +++ b/vlib/v/checker/tests/string_interpolation_wrong_fmt.vv @@ -0,0 +1,24 @@ +fn interpolate_str() string { + a := 'hallo' + x := '>${a:8.3s}<' + y := '${a:G}' + z := '${a:d}' + return x + y + z +} + +fn interpolate_f64() string { + b := 1367.57 + x := '>${b:20s}<' + y := '${b:d}' + return x + y +} + +fn interpolate_int() string { + u := u32(15) + s := -12 + x := '${u:13d}' + y := '${s:04u}' + z := '${s:f}' + q := '${u:v}' + return x + y + z + q +} diff --git a/vlib/v/fmt/fmt.v b/vlib/v/fmt/fmt.v index 58d9c777b0..33af4dbc9c 100644 --- a/vlib/v/fmt/fmt.v +++ b/vlib/v/fmt/fmt.v @@ -721,10 +721,28 @@ pub fn (mut f Fmt) expr(node ast.Expr) { continue } f.write('$') - if it.expr_fmts[i].len > 0 { + needs_fspec := it.need_fmts[i] || it.pluss[i] || (it.fills[i] && it.fwidths[i] >= 0) || it.fwidths[i] != 0 || it.precisions[i] != 0 + if needs_fspec || (it.exprs[i] !is ast.Ident && it.exprs[i] !is ast.SelectorExpr) { f.write('{') f.expr(it.exprs[i]) - f.write(it.expr_fmts[i]) + if needs_fspec { + f.write(':') + if it.pluss[i] { + f.write('+') + } + if it.fills[i] && it.fwidths[i] >= 0 { + f.write('0') + } + if it.fwidths[i] != 0 { + f.write('${it.fwidths[i]}') + } + if it.precisions[i] != 0 { + f.write('.${it.precisions[i]}') + } + if it.need_fmts[i] { + f.write('${it.fmts[i]:c}') + } + } f.write('}') } else { f.expr(it.exprs[i]) diff --git a/vlib/v/gen/cgen.v b/vlib/v/gen/cgen.v index f6755f21f5..6c4e36a78a 100644 --- a/vlib/v/gen/cgen.v +++ b/vlib/v/gen/cgen.v @@ -4,7 +4,6 @@ module gen import strings -import strconv import v.ast import v.table import v.pref @@ -2918,101 +2917,40 @@ fn (g Gen) sort_structs(typesa []table.TypeSymbol) []table.TypeSymbol { } fn (mut g Gen) string_inter_literal(node ast.StringInterLiteral) { - //if g.pref.autofree { - //g.write('_STR_TMP("') - //} else { g.write('_STR("') - //} // Build the string with % - mut fieldwidths := []int{} - mut specs := []byte{} mut end_string := false for i, val in node.vals { escaped_val := val.replace_each(['"', '\\"', '\r\n', '\\n', '\n', '\\n', '%', '%%']) if i >= node.exprs.len { if escaped_val.len > 0 { end_string = true - //if !g.pref.autofree { - g.write('\\000') - //} + g.write('\\000') g.write(escaped_val) } - continue + break } g.write(escaped_val) - 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 { - // analyze and validate format specifier - if sfmt[sfmt.len - 1] in [`E`, `F`, `G`, `e`, `f`, `g`, - `d`, `u`, `x`, `X`, `o`, `c`, `s`, `p`] { - fspec = sfmt[sfmt.len - 1] - } - fmt = if fspec == `_` { - sfmt[1..sfmt.len] - } else { - sfmt[1..sfmt.len - 1] - } - } - 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() || node.expr_types[i].is_any_int() { - fspec = `d` - } else if node.expr_types[i].is_unsigned() { - fspec = `u` - } else if node.expr_types[i].is_pointer() { - fspec = `p` - } 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_any_int() && fspec !in [`d`, `c`, `x`, `X`, - `o`, `u`, - `x`, `X`, `o`] || node.expr_types[i].is_float() && fspec !in [`E`, `F`, `G`, `e`, - `f`, `g`] || node.expr_types[i].is_pointer() && fspec !in [`p`, `x`, `X`] { - 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('%') + fspec := node.fmts[i] + mut fmt := if node.pluss[i] { '+' } else { '' } + if node.fills[i] && node.fwidths[i] >= 0 { + fmt = '${fmt}0' + } + if node.fwidths[i] != 0 { + fmt = '$fmt${node.fwidths[i]}' + } + if node.precisions[i] != 0 { + fmt = '${fmt}.${node.precisions[i]}' + } if fspec == `s` { - if fields.len == 0 || strconv.atoi(fields[0]) == 0 { + if node.fwidths[i] == 0 { g.write('.*s') } else { g.write('*.*s') } - } else if node.expr_types[i].is_float() || node.expr_types[i].is_pointer() { + } else if node.expr_types[i].is_float() { g.write('$fmt${fspec:c}') } else if node.expr_types[i].is_pointer() { if fspec == `p` { @@ -3022,11 +2960,7 @@ fn (mut g Gen) string_inter_literal(node ast.StringInterLiteral) { } } 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') - } + g.write('${fmt}c') } else { g.write('${fmt}"PRI${fspec:c}') if node.expr_types[i] in [table.i8_type, table.byte_type] { @@ -3057,9 +2991,9 @@ fn (mut g Gen) string_inter_literal(node ast.StringInterLiteral) { } else if node.expr_types[i] == table.bool_type { g.expr(expr) g.write(' ? _SLIT("true") : _SLIT("false")') - } else if node.expr_types[i].is_number() || node.expr_types[i].is_pointer() || specs[i] == + } else if node.expr_types[i].is_number() || node.expr_types[i].is_pointer() || node.fmts[i] == `d` { - if node.expr_types[i].is_signed() && specs[i] in [`x`, `X`, `o`] { + if node.expr_types[i].is_signed() && node.fmts[i] in [`x`, `X`, `o`] { // convert to unsigned first befors C's integer propagation strikes if node.expr_types[i] == table.i8_type { g.write('(byte)(') @@ -3075,13 +3009,13 @@ fn (mut g Gen) string_inter_literal(node ast.StringInterLiteral) { } else { g.expr(expr) } - } else if specs[i] == `s` { + } else if node.fmts[i] == `s` { g.gen_expr_to_string(expr, node.expr_types[i]) } else { g.expr(expr) } - if specs[i] == `s` && fieldwidths[i] != 0 { - g.write(', ${fieldwidths[i]}') + if node.fmts[i] == `s` && node.fwidths[i] != 0 { + g.write(', ${node.fwidths[i]}') } if i < node.exprs.len - 1 { g.write(', ') diff --git a/vlib/v/gen/js/js.v b/vlib/v/gen/js/js.v index 32da39071b..a36c370f3c 100644 --- a/vlib/v/gen/js/js.v +++ b/vlib/v/gen/js/js.v @@ -1367,9 +1367,11 @@ fn (mut g JsGen) gen_string_inter_literal(it ast.StringInterLiteral) { continue } expr := it.exprs[i] - sfmt := it.expr_fmts[i] + fmt := it.fmts[i] + fwidth := it.fwidths[i] + precision := it.precisions[i] g.write('\${') - if sfmt.len > 0 { + if fmt != `_` || fwidth !=0 || precision != 0 { // TODO: Handle formatting g.expr(expr) } else { diff --git a/vlib/v/parser/parser.v b/vlib/v/parser/parser.v index 2fe552755e..1db78c45a8 100644 --- a/vlib/v/parser/parser.v +++ b/vlib/v/parser/parser.v @@ -13,6 +13,7 @@ import v.errors import os import runtime import time +import strconv pub struct Parser { file_name string // "/home/user/hello.v" @@ -1166,7 +1167,13 @@ fn (mut p Parser) string_expr() ast.Expr { } mut exprs := []ast.Expr{} mut vals := []string{} - mut efmts := []string{} + mut has_fmts := []bool{} + mut fwidths := []int{} + mut precisions := []int{} + mut visible_pluss := []bool{} + mut fills := []bool{} + mut fmts := []byte{} + mut fposs := []token.Position{} // Handle $ interpolation p.inside_str_interp = true for p.tok.kind == .string { @@ -1177,31 +1184,66 @@ fn (mut p Parser) string_expr() ast.Expr { } p.next() exprs << p.expr(0) - mut efmt := []string{} + mut has_fmt := false + mut fwidth := 0 + mut fwidthneg := false + mut precision := 0 + mut visible_plus := false + mut fill := false + mut fmt := `_` // placeholder if p.tok.kind == .colon { - efmt << ':' + has_fmt = true p.next() // ${num:-2d} if p.tok.kind == .minus { - efmt << '-' + fwidthneg = true + p.next() + } else if p.tok.kind == .plus { + visible_plus = true p.next() } // ${num:2d} if p.tok.kind == .number { - efmt << p.tok.lit + fields := p.tok.lit.split('.') + if fields[0].len > 0 && fields[0][0] == `0` { + fill = true + } + fwidth = strconv.atoi(fields[0]) + if fwidthneg { + fwidth = -fwidth + } + if fields.len > 1 { + precision = strconv.atoi(fields[1]) + } p.next() } - if p.tok.kind == .name && p.tok.lit.len == 1 { - efmt << p.tok.lit - p.next() + if p.tok.kind == .name { + if p.tok.lit.len == 1 { + fmt = p.tok.lit[0] + p.next() + } else { + p.error('format specifier may only be one letter') + } } } - efmts << efmt.join('') + fwidths << fwidth + has_fmts << has_fmt + precisions << precision + visible_pluss << visible_plus + fmts << fmt + fills << fill + fposs << p.prev_tok.position() } node = ast.StringInterLiteral{ vals: vals exprs: exprs - expr_fmts: efmts + need_fmts: has_fmts // prelimery - until checker finds out if really needed + fwidths: fwidths + precisions: precisions + pluss: visible_pluss + fills: fills + fmts: fmts + fmt_poss: fposs pos: pos } p.inside_str_interp = false diff --git a/vlib/v/table/atypes.v b/vlib/v/table/atypes.v index 5ba36a5552..1882376a46 100644 --- a/vlib/v/table/atypes.v +++ b/vlib/v/table/atypes.v @@ -187,6 +187,11 @@ pub fn (typ Type) is_number() bool { return typ.idx() in number_type_idxs } +[inline] +pub fn (typ Type) is_string() bool { + return typ.idx() in string_type_idxs +} + pub const ( void_type_idx = 1 voidptr_type_idx = 2