Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

transformer: optimise calls to write string with simple interpolations #22188

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion vlib/strings/builder.c.v
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions vlib/v/ast/ast.v
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,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 {
Expand Down Expand Up @@ -682,6 +684,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 {
Expand Down Expand Up @@ -805,6 +811,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.
}

/*
Expand Down
19 changes: 19 additions & 0 deletions vlib/v/checker/fn.v
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,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
Expand Down Expand Up @@ -1180,6 +1197,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) {
Expand Down Expand Up @@ -2330,6 +2348,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) {
Expand Down
18 changes: 16 additions & 2 deletions vlib/v/gen/c/text_manipulation.v
Original file line number Diff line number Diff line change
Expand Up @@ -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 } {
Expand All @@ -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: <nil> | 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
}

Expand Down
10 changes: 10 additions & 0 deletions vlib/v/parser/fn.v
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {}
}
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
})
}
/*
Expand Down Expand Up @@ -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())
Expand Down
91 changes: 91 additions & 0 deletions vlib/v/transformer/transformer.v
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)
Expand Down Expand Up @@ -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 {}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -1152,3 +1159,87 @@ 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 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
}
Loading