diff --git a/vlib/strings/builder.c.v b/vlib/strings/builder.c.v index 24b6c645c0c106..1370791fe1ffec 100644 --- a/vlib/strings/builder.c.v +++ b/vlib/strings/builder.c.v @@ -131,7 +131,7 @@ pub fn (b &Builder) byte_at(n int) u8 { } // write appends the string `s` to the buffer -@[inline] +@[expand_simple_interpolation; inline] pub fn (mut b Builder) write_string(s string) { if s.len == 0 { return diff --git a/vlib/v/ast/ast.v b/vlib/v/ast/ast.v index 247214c5f13c09..a3055df6029ab3 100644 --- a/vlib/v/ast/ast.v +++ b/vlib/v/ast/ast.v @@ -615,6 +615,8 @@ pub mut: scope &Scope = unsafe { nil } label_names []string pos token.Pos // function declaration position + // + is_expand_simple_interpolation bool // true, when @[expand_simple_interpolation] is used on a fn. It should have a single string argument. } pub fn (f &FnDecl) new_method_with_receiver_type(new_type_ Type) FnDecl { @@ -687,6 +689,10 @@ pub mut: ctdefine_idx int // the index of the attribute, containing the compile time define [if mytag] from_embedded_type Type // for interface only, fn from the embedded interface from_embeded_type Type @[deprecated: 'use from_embedded_type instead'; deprecated_after: '2024-03-31'] + // + is_expand_simple_interpolation bool // for tagging b.f(s string), which is then called with `b.f('some $x $y')`, + // when that call, should be expanded to `b.f('some '); b.f(x); b.f(' '); b.f(y);` + // Note: the same type, has to support also a .write_decimal(n i64) method. } fn (f &Fn) method_equals(o &Fn) bool { @@ -810,6 +816,10 @@ pub mut: scope &Scope = unsafe { nil } from_embed_types []Type // holds the type of the embed that the method is called from comments []Comment + // + is_expand_simple_interpolation bool // true, when the function/method is marked as @[expand_simple_interpolation] + // Calls to it with an interpolation argument like `b.f('x ${y}')`, will be converted to `b.f('x ')` followed by `b.f(y)`. + // The same type, has to support also a .write_decimal(n i64) method. } /* diff --git a/vlib/v/checker/fn.v b/vlib/v/checker/fn.v index 080af30ca3c40d..a01209d19a74b0 100644 --- a/vlib/v/checker/fn.v +++ b/vlib/v/checker/fn.v @@ -542,6 +542,23 @@ fn (mut c Checker) fn_decl(mut node ast.FnDecl) { } } } + if node.is_expand_simple_interpolation { + match true { + !node.is_method { + c.error('@[expand_simple_interpolation] is supported only on methods', + node.pos) + } + node.params.len != 2 { + c.error('methods tagged with @[expand_simple_interpolation], should have exactly 1 argument', + node.pos) + } + !node.params[1].typ.is_string() { + c.error('methods tagged with @[expand_simple_interpolation], should accept a single string', + node.pos) + } + else {} + } + } } // check_same_type_ignoring_pointers util function to check if the Types are the same, including all @@ -1194,6 +1211,7 @@ fn (mut c Checker) fn_call(mut node ast.CallExpr, mut continue_check &bool) ast. } node.is_file_translated = func.is_file_translated node.is_noreturn = func.is_noreturn + node.is_expand_simple_interpolation = func.is_expand_simple_interpolation node.is_ctor_new = func.is_ctor_new if !found_in_args { if node.scope.known_var(fn_name) { @@ -2325,6 +2343,7 @@ fn (mut c Checker) method_call(mut node ast.CallExpr) ast.Type { c.need_recheck_generic_fns = true } node.is_noreturn = method.is_noreturn + node.is_expand_simple_interpolation = method.is_expand_simple_interpolation node.is_ctor_new = method.is_ctor_new node.return_type = method.return_type if method.return_type.has_flag(.generic) { diff --git a/vlib/v/gen/c/text_manipulation.v b/vlib/v/gen/c/text_manipulation.v index 93ac00041b40ec..6d4ec11bbe0c37 100644 --- a/vlib/v/gen/c/text_manipulation.v +++ b/vlib/v/gen/c/text_manipulation.v @@ -5,6 +5,7 @@ module c import v.util +@[expand_simple_interpolation] fn (mut g Gen) write(s string) { $if trace_gen ? { if g.file == unsafe { nil } { @@ -15,10 +16,23 @@ fn (mut g Gen) write(s string) { } if g.indent > 0 && g.empty_line { g.out.write_string(util.tabs(g.indent)) - // g.out_parallel[g.out_idx].write_string(util.tabs(g.indent)) } g.out.write_string(s) - ////g.out_parallel[g.out_idx].write_string(s) + g.empty_line = false +} + +fn (mut g Gen) write_decimal(x i64) { + $if trace_gen ? { + if g.file == unsafe { nil } { + eprintln('gen file: | last_fn_c_name: ${g.last_fn_c_name:-45} | write_decimal: ${x}') + } else { + eprintln('gen file: ${g.file.path:-30} | last_fn_c_name: ${g.last_fn_c_name:-45} | write_decimal: ${x}') + } + } + if g.indent > 0 && g.empty_line { + g.out.write_string(util.tabs(g.indent)) + } + g.out.write_decimal(x) g.empty_line = false } diff --git a/vlib/v/parser/fn.v b/vlib/v/parser/fn.v index 699779ff515fd7..b91a7eb6d9cbab 100644 --- a/vlib/v/parser/fn.v +++ b/vlib/v/parser/fn.v @@ -197,6 +197,7 @@ fn (mut p Parser) fn_decl() ast.FnDecl { mut is_ctor_new := false mut is_c2v_variadic := false mut is_markused := false + mut is_expand_simple_interpolation := false mut comments := []ast.Comment{} for fna in p.attrs { match fna.name { @@ -254,6 +255,9 @@ fn (mut p Parser) fn_decl() ast.FnDecl { p.prev_tok.pos()) } } + 'expand_simple_interpolation' { + is_expand_simple_interpolation = true + } else {} } } @@ -550,6 +554,8 @@ run them via `v file.v` instead', pos: start_pos name_pos: name_pos language: language + // + is_expand_simple_interpolation: is_expand_simple_interpolation }) } else { name = match language { @@ -603,6 +609,8 @@ run them via `v file.v` instead', pos: start_pos name_pos: name_pos language: language + // + is_expand_simple_interpolation: is_expand_simple_interpolation }) } /* @@ -688,6 +696,8 @@ run them via `v file.v` instead', label_names: p.label_names end_comments: p.eat_comments(same_line: true) comments: comments + // + is_expand_simple_interpolation: is_expand_simple_interpolation } if generic_names.len > 0 { p.table.register_fn_generic_types(fn_decl.fkey()) diff --git a/vlib/v/transformer/transformer.v b/vlib/v/transformer/transformer.v index 8fdcd7eab716f4..28dfa12aea0e3a 100644 --- a/vlib/v/transformer/transformer.v +++ b/vlib/v/transformer/transformer.v @@ -13,6 +13,8 @@ pub mut: mut: is_assert bool inside_dump bool + // + strings_builder_type ast.Type = ast.no_type } fn (mut t Transformer) trace[T](fbase string, x &T) { @@ -38,6 +40,7 @@ pub fn new_transformer_with_table(table &ast.Table, pref_ &pref.Preferences) &Tr } pub fn (mut t Transformer) transform_files(ast_files []&ast.File) { + t.strings_builder_type = t.table.find_type_idx('strings.Builder') for i in 0 .. ast_files.len { mut file := unsafe { ast_files[i] } t.transform(mut file) @@ -178,6 +181,7 @@ pub fn (mut t Transformer) stmt(mut node ast.Stmt) ast.Stmt { ntype := typeof(*node).replace('v.ast.', '') eprintln('transformer: ${t.file.path:-50} | pos: ${node.pos.line_str():-39} | node: ${ntype:12} | ${node}') } + mut onode := unsafe { node } match mut node { ast.EmptyStmt {} ast.NodeError {} @@ -244,6 +248,9 @@ pub fn (mut t Transformer) stmt(mut node ast.Stmt) ast.Stmt { t.expr(mut node.expr) } } + if mut node.expr is ast.CallExpr && node.expr.is_expand_simple_interpolation { + t.simplify_nested_interpolation_in_sb(mut onode, mut node.expr, node.typ) + } } ast.FnDecl { if t.pref.trace_calls { @@ -1152,3 +1159,90 @@ pub fn (mut t Transformer) fn_decl_trace_calls(mut node ast.FnDecl) { } node.stmts.prepend(expr_stmt) } + +pub fn (mut t Transformer) simplify_nested_interpolation_in_sb(mut onode ast.Stmt, mut nexpr ast.CallExpr, ntype ast.Type) bool { + if t.pref.autofree { + return false + } + if nexpr.args[0].expr !is ast.StringInterLiteral { + return false + } + original := nexpr.args[0].expr as ast.StringInterLiteral + // only very simple string interpolations, without any formatting, like the following examples + // can be optimised to a list of simpler string builder calls, instead of using str_intp: + // >> sb.write_string('abc ${num}') + // >> sb.write_string('abc ${num} ${some_string} ${another_string} end') + for idx, w in original.fwidths { + if w != 0 { + return false + } + if original.precisions[idx] != 987698 { + return false + } + if original.need_fmts[idx] { + return false + } + // good ... no complex formatting found; now check the types (only strings and non float numbers are supported) + if original.expr_types[idx] == ast.string_type { + continue + } + if !original.expr_types[idx].is_int() { + return false + } + } + + // first, insert all the statements, for writing the static strings, that were parts of the original string interpolation: + mut calls := []ast.Stmt{} + for val in original.vals { + if val == '' { + // there is no point in appending empty strings + // so instead, just emit an empty statement, to be ignored by the backend + calls << ast.EmptyStmt{} + continue + } + mut ncall := ast.ExprStmt{ + expr: ast.Expr(ast.CallExpr{ + ...nexpr + args: [ + ast.CallArg{ + ...nexpr.args[0] + expr: ast.StringLiteral{ + val: val + } + }, + ] + }) + typ: ntype + } + calls << ncall + } + // now, insert the statements for writing the variable expressions between the static strings: + for idx, expr in original.exprs { + mut ncall := ast.ExprStmt{ + typ: ntype + expr: ast.Expr(ast.CallExpr{ + ...nexpr + args: [ + ast.CallArg{ + ...nexpr.args[0] + expr: expr + }, + ] + }) + } + etype := original.expr_types[idx] + if etype.is_int() { + if mut ncall.expr is ast.CallExpr { + ncall.expr.name = 'write_decimal' + } + } + calls.insert(1 + 2 * idx, ncall) // the new statements should be between the existing ones for static strings + } + // calls << ast.node + unsafe { + *onode = ast.Stmt(ast.Block{ + stmts: calls + }) + } + return true +}