autofree: lots of fixes

pull/6314/head
Alexander Medvednikov 2020-09-05 12:00:35 +02:00
parent b015033c53
commit 3410705974
8 changed files with 186 additions and 127 deletions

View File

@ -216,16 +216,16 @@ jobs:
# github-token: ${{ secrets.GITHUB_TOKEN }} # github-token: ${{ secrets.GITHUB_TOKEN }}
ubuntu-autofree-selfcompile: # ubuntu-autofree-selfcompile:
runs-on: ubuntu-18.04 # runs-on: ubuntu-18.04
env: # env:
VFLAGS: -cc gcc # VFLAGS: -cc gcc
steps: # steps:
- uses: actions/checkout@v2 # - uses: actions/checkout@v2
- name: Build V # - name: Build V
run: make -j4 # run: make -j4
- name: V self compilation with -autofree # - name: V self compilation with -autofree
run: ./v -o v2 -autofree cmd/v && ./v2 -o v3 -autofree cmd/v && ./v3 -o v4 -autofree cmd/v # run: ./v -o v2 -autofree cmd/v && ./v2 -o v3 -autofree cmd/v && ./v3 -o v4 -autofree cmd/v
# Ubuntu docker pre-built container # Ubuntu docker pre-built container

View File

@ -153,52 +153,52 @@ fn unescape(s_ string, mode EncodingMode) ?string {
for i := 0; i < s.len; { for i := 0; i < s.len; {
x := s[i] x := s[i]
match x { match x {
`%` { `%` {
if s == '' { if s == '' {
break break
}
n++
if i + 2 >= s.len || !ishex(s[i + 1]) || !ishex(s[i + 2]) {
s = s[i..]
if s.len > 3 {
s = s[..3]
}
return error(error_msg(err_msg_escape, s))
}
// Per https://tools.ietf.org/html/rfc3986#page-21
// in the host component %-encoding can only be used
// for non-ASCII bytes.
// But https://tools.ietf.org/html/rfc6874#section-2
// introduces %25 being allowed to escape a percent sign
// in IPv6 scoped-address literals. Yay.
if mode == .encode_host && unhex(s[i + 1]) < 8 && s[i..i + 3] != '%25' {
return error(error_msg(err_msg_escape, s[i..i + 3]))
}
if mode == .encode_zone {
// RFC 6874 says basically 'anything goes' for zone identifiers
// and that even non-ASCII can be redundantly escaped,
// but it seems prudent to restrict %-escaped bytes here to those
// that are valid host name bytes in their unescaped form.
// That is, you can use escaping in the zone identifier but not
// to introduce bytes you couldn't just write directly.
// But Windows puts spaces here! Yay.
v := ( (unhex(s[i + 1])<<byte(4)) | unhex(s[i + 2]))
if s[i..i + 3] != '%25' && v != ` ` && should_escape(v, .encode_host) {
error(error_msg(err_msg_escape, s[i..i + 3]))
}
}
i += 3
} }
`+` { n++
has_plus = mode == .encode_query_component if i + 2 >= s.len || !ishex(s[i + 1]) || !ishex(s[i + 2]) {
i++ s = s[i..]
} if s.len > 3 {
else { s = s[..3]
if (mode == .encode_host || mode == .encode_zone) && s[i] < 0x80 && should_escape(s[i], mode) {
error(error_msg('unescape: invalid character in host name', s[i..i + 1]))
} }
i++ return error(error_msg(err_msg_escape, s))
}} }
// Per https://tools.ietf.org/html/rfc3986#page-21
// in the host component %-encoding can only be used
// for non-ASCII bytes.
// But https://tools.ietf.org/html/rfc6874#section-2
// introduces %25 being allowed to escape a percent sign
// in IPv6 scoped-address literals. Yay.
if mode == .encode_host && unhex(s[i + 1]) < 8 && s[i..i + 3] != '%25' {
return error(error_msg(err_msg_escape, s[i..i + 3]))
}
if mode == .encode_zone {
// RFC 6874 says basically 'anything goes' for zone identifiers
// and that even non-ASCII can be redundantly escaped,
// but it seems prudent to restrict %-escaped bytes here to those
// that are valid host name bytes in their unescaped form.
// That is, you can use escaping in the zone identifier but not
// to introduce bytes you couldn't just write directly.
// But Windows puts spaces here! Yay.
v := ( (unhex(s[i + 1])<<byte(4)) | unhex(s[i + 2]))
if s[i..i + 3] != '%25' && v != ` ` && should_escape(v, .encode_host) {
error(error_msg(err_msg_escape, s[i..i + 3]))
}
}
i += 3
}
`+` {
has_plus = mode == .encode_query_component
i++
}
else {
if (mode == .encode_host || mode == .encode_zone) && s[i] < 0x80 && should_escape(s[i], mode) {
error(error_msg('unescape: invalid character in host name', s[i..i + 1]))
}
i++
}}
} }
if n == 0 && !has_plus { if n == 0 && !has_plus {
return s return s

View File

@ -10,11 +10,11 @@ import v.errors
pub type TypeDecl = AliasTypeDecl | FnTypeDecl | SumTypeDecl pub type TypeDecl = AliasTypeDecl | FnTypeDecl | SumTypeDecl
pub type Expr = AnonFn | ArrayInit | AsCast | Assoc | BoolLiteral | CallExpr | CastExpr | pub type Expr = AnonFn | ArrayInit | AsCast | Assoc | BoolLiteral | CallExpr | CastExpr |
CharLiteral | ChanInit | Comment | ComptimeCall | ConcatExpr | EnumVal | FloatLiteral | Ident | IfExpr | ChanInit | CharLiteral | Comment | ComptimeCall | ConcatExpr | EnumVal | FloatLiteral |
IfGuardExpr | IndexExpr | InfixExpr | IntegerLiteral | Likely | LockExpr | MapInit | MatchExpr | Ident | IfExpr | IfGuardExpr | IndexExpr | InfixExpr | IntegerLiteral | Likely | LockExpr |
None | OrExpr | ParExpr | PostfixExpr | PrefixExpr | RangeExpr | SelectorExpr | SizeOf | MapInit | MatchExpr | None | OrExpr | ParExpr | PostfixExpr | PrefixExpr | RangeExpr |
SqlExpr | StringInterLiteral | StringLiteral | StructInit | Type | TypeOf | UnsafeExpr SelectorExpr | SizeOf | SqlExpr | StringInterLiteral | StringLiteral | StructInit | Type |
TypeOf | UnsafeExpr
pub type Stmt = AssertStmt | AssignStmt | Block | BranchStmt | CompFor | CompIf | ConstDecl | pub type Stmt = AssertStmt | AssignStmt | Block | BranchStmt | CompFor | CompIf | ConstDecl |
DeferStmt | EnumDecl | ExprStmt | FnDecl | ForCStmt | ForInStmt | ForStmt | GlobalDecl | DeferStmt | EnumDecl | ExprStmt | FnDecl | ForCStmt | ForInStmt | ForStmt | GlobalDecl |
@ -158,10 +158,10 @@ pub mut:
pub struct ConstDecl { pub struct ConstDecl {
pub: pub:
is_pub bool is_pub bool
pos token.Position pos token.Position
pub mut: pub mut:
fields []ConstField fields []ConstField
end_comments []Comment end_comments []Comment
} }
@ -289,16 +289,26 @@ pub mut:
return_type table.Type return_type table.Type
should_be_skipped bool should_be_skipped bool
generic_type table.Type // TODO array, to support multiple types generic_type table.Type // TODO array, to support multiple types
// autofree_vars []AutofreeArgVar
// autofree_vars_ids []int
} }
/*
pub struct AutofreeArgVar {
name string
idx int
}
*/
pub struct CallArg { pub struct CallArg {
pub: pub:
is_mut bool is_mut bool
share table.ShareType share table.ShareType
expr Expr expr Expr
comments []Comment comments []Comment
pub mut: pub mut:
typ table.Type typ table.Type
is_tmp_autofree bool // for autofree
// tmp_name string // for autofree
} }
pub struct Return { pub struct Return {
@ -761,12 +771,12 @@ pub mut:
pub struct ChanInit { pub struct ChanInit {
pub: pub:
pos token.Position pos token.Position
cap_expr Expr cap_expr Expr
has_cap bool has_cap bool
pub mut: pub mut:
typ table.Type typ table.Type
elem_type table.Type elem_type table.Type
} }
pub struct MapInit { pub struct MapInit {
@ -1069,9 +1079,9 @@ pub fn (expr Expr) position() token.Position {
pub fn (expr Expr) is_lvalue() bool { pub fn (expr Expr) is_lvalue() bool {
match expr { match expr {
Ident {return true} Ident { return true }
IndexExpr {return expr.left.is_lvalue()} IndexExpr { return expr.left.is_lvalue() }
SelectorExpr {return expr.expr.is_lvalue()} SelectorExpr { return expr.expr.is_lvalue() }
else {} else {}
} }
return false return false

View File

@ -846,10 +846,22 @@ fn (mut c Checker) fail_if_immutable(expr ast.Expr) (string, token.Position) {
pub fn (mut c Checker) call_expr(mut call_expr ast.CallExpr) table.Type { pub fn (mut c Checker) call_expr(mut call_expr ast.CallExpr) table.Type {
c.stmts(call_expr.or_block.stmts) c.stmts(call_expr.or_block.stmts)
if call_expr.is_method { typ := if call_expr.is_method { c.call_method(call_expr) } else { c.call_fn(call_expr) }
return c.call_method(call_expr) // autofree
free_tmp_arg_vars := c.pref.autofree && c.pref.experimental && !c.is_builtin_mod &&
call_expr.args.len > 0 && !call_expr.args[0].typ.has_flag(.optional)
if free_tmp_arg_vars {
for i, arg in call_expr.args {
if arg.typ != table.string_type {
continue
}
if arg.expr is ast.Ident || arg.expr is ast.StringLiteral {
continue
}
call_expr.args[i].is_tmp_autofree = true
}
} }
return c.call_fn(call_expr) return typ
} }
fn (mut c Checker) check_map_and_filter(is_map bool, elem_typ table.Type, call_expr ast.CallExpr) { fn (mut c Checker) check_map_and_filter(is_map bool, elem_typ table.Type, call_expr ast.CallExpr) {
@ -2228,7 +2240,7 @@ fn (mut c Checker) stmt(node ast.Stmt) {
} }
ast.Module { ast.Module {
c.mod = node.name c.mod = node.name
c.is_builtin_mod = node.name == 'builtin' c.is_builtin_mod = node.name in ['builtin', 'os', 'strconv']
c.check_valid_snake_case(node.name, 'module name', node.pos) c.check_valid_snake_case(node.name, 'module name', node.pos)
} }
ast.Return { ast.Return {

View File

@ -53,7 +53,8 @@ mut:
file ast.File file ast.File
fn_decl &ast.FnDecl // pointer to the FnDecl we are currently inside otherwise 0 fn_decl &ast.FnDecl // pointer to the FnDecl we are currently inside otherwise 0
last_fn_c_name string last_fn_c_name string
tmp_count int tmp_count int // counter for unique tmp vars (_tmp1, tmp2 etc)
tmp_count2 int // a separate tmp var counter for autofree fn calls
variadic_args map[string]int variadic_args map[string]int
is_c_call bool // e.g. `C.printf("v")` is_c_call bool // e.g. `C.printf("v")`
is_assign_lhs bool // inside left part of assign expr (for array_set(), etc) is_assign_lhs bool // inside left part of assign expr (for array_set(), etc)
@ -106,9 +107,10 @@ mut:
comptime_var_type_map map[string]table.Type comptime_var_type_map map[string]table.Type
match_sumtype_exprs []ast.Expr match_sumtype_exprs []ast.Expr
match_sumtype_syms []table.TypeSymbol match_sumtype_syms []table.TypeSymbol
tmp_arg_vars_to_free []string // tmp_arg_vars_to_free []string
called_fn_name string called_fn_name string
cur_mod string cur_mod string
is_js_call bool // for handling a special type arg #1 `json.decode(User, ...)`
} }
const ( const (
@ -663,6 +665,12 @@ pub fn (mut g Gen) new_tmp_var() string {
return '_t$g.tmp_count' return '_t$g.tmp_count'
} }
/*
pub fn (mut g Gen) new_tmp_var2() string {
g.tmp_count2++
return '_tt$g.tmp_count2'
}
*/
pub fn (mut g Gen) reset_tmp_count() { pub fn (mut g Gen) reset_tmp_count() {
g.tmp_count = 0 g.tmp_count = 0
} }
@ -717,8 +725,9 @@ fn (mut g Gen) write_v_source_line_info(pos token.Position) {
fn (mut g Gen) stmt(node ast.Stmt) { fn (mut g Gen) stmt(node ast.Stmt) {
g.stmt_path_pos << g.out.len g.stmt_path_pos << g.out.len
/*
defer { defer {
// If have temporary string exprs to free after this statement, do it. e.g.: // If we have temporary string exprs to free after this statement, do it. e.g.:
// `foo('a' + 'b')` => `tmp := 'a' + 'b'; foo(tmp); string_free(&tmp);` // `foo('a' + 'b')` => `tmp := 'a' + 'b'; foo(tmp); string_free(&tmp);`
if g.pref.autofree { if g.pref.autofree {
if g.strs_to_free != '' { if g.strs_to_free != '' {
@ -727,6 +736,7 @@ fn (mut g Gen) stmt(node ast.Stmt) {
} }
} }
} }
*/
// println('cgen.stmt()') // println('cgen.stmt()')
// g.writeln('//// stmt start') // g.writeln('//// stmt start')
match node { match node {
@ -932,7 +942,8 @@ fn (mut g Gen) stmt(node ast.Stmt) {
// definitions are sorted and added in write_types // definitions are sorted and added in write_types
} }
ast.Module { ast.Module {
g.is_builtin_mod = node.name == 'builtin' // g.is_builtin_mod = node.name == 'builtin'
g.is_builtin_mod = node.name in ['builtin', 'os', 'strconv']
g.cur_mod = node.name g.cur_mod = node.name
} }
ast.Return { ast.Return {

View File

@ -340,7 +340,8 @@ fn (mut g Gen) method_call(node ast.CallExpr) {
g.write('${dot}_object') g.write('${dot}_object')
if node.args.len > 0 { if node.args.len > 0 {
g.write(', ') g.write(', ')
g.call_args(node.args, node.expected_arg_types) // g.call_args(node.args, node.expected_arg_types) // , [])
g.call_args(node)
} }
g.write(')') g.write(')')
return return
@ -449,7 +450,8 @@ fn (mut g Gen) method_call(node ast.CallExpr) {
} }
*/ */
// /////// // ///////
g.call_args(node.args, node.expected_arg_types) // g.call_args(node.args, node.expected_arg_types) // , [])
g.call_args(node)
g.write(')') g.write(')')
} }
@ -485,7 +487,8 @@ fn (mut g Gen) fn_call(node ast.CallExpr) {
encode_name := c_name(name) + '_' + util.no_dots(json_type_str) encode_name := c_name(name) + '_' + util.no_dots(json_type_str)
g.writeln('// json.encode') g.writeln('// json.encode')
g.write('cJSON* $json_obj = ${encode_name}(') g.write('cJSON* $json_obj = ${encode_name}(')
g.call_args(node.args, node.expected_arg_types) // g.call_args(node.args, node.expected_arg_types) // , [])
g.call_args(node)
g.writeln(');') g.writeln(');')
tmp2 = g.new_tmp_var() tmp2 = g.new_tmp_var()
g.writeln('string $tmp2 = json__json_print($json_obj);') g.writeln('string $tmp2 = json__json_print($json_obj);')
@ -499,7 +502,10 @@ fn (mut g Gen) fn_call(node ast.CallExpr) {
g.write('cJSON* $json_obj = json__json_parse(') g.write('cJSON* $json_obj = json__json_parse(')
// Skip the first argument in json.decode which is a type // Skip the first argument in json.decode which is a type
// its name was already used to generate the function call // its name was already used to generate the function call
g.call_args(node.args[1..], node.expected_arg_types) // g.call_args(node.args[1..], node.expected_arg_types) // , [])
g.is_js_call = true
g.call_args(node)
g.is_js_call = false
g.writeln(');') g.writeln(');')
tmp2 = g.new_tmp_var() tmp2 = g.new_tmp_var()
g.writeln('Option_$typ $tmp2 = $fn_name ($json_obj);') g.writeln('Option_$typ $tmp2 = $fn_name ($json_obj);')
@ -522,27 +528,26 @@ fn (mut g Gen) fn_call(node ast.CallExpr) {
} }
// Create a temporary var for each argument in order to free it (only if it's a complex expression, // Create a temporary var for each argument in order to free it (only if it's a complex expression,
// like `foo(get_string())` or `foo(a + b)` // like `foo(get_string())` or `foo(a + b)`
free_tmp_arg_vars := g.autofree && g.pref.experimental && !g.is_builtin_mod && g.cur_mod != mut free_tmp_arg_vars := g.autofree && g.pref.experimental && !g.is_builtin_mod &&
'os' && node.args.len > 0 && !node.args[0].typ.has_flag(.optional) node.args.len > 0 && !node.args[0].typ.has_flag(.optional) // TODO copy pasta checker.v
mut cur_line := ''
if free_tmp_arg_vars { if free_tmp_arg_vars {
g.tmp_arg_vars_to_free = []string{cap: 10} // TODO perf free_tmp_arg_vars = false // set the flag to true only if we have at least one arg to free
g.tmp_count2++
for i, arg in node.args { for i, arg in node.args {
if arg.typ != table.string_type { if !arg.is_tmp_autofree {
continue continue
} }
if arg.expr is ast.Ident || arg.expr is ast.StringLiteral { free_tmp_arg_vars = true
continue // t := g.new_tmp_var() + '_arg_expr_${name}_$i'
} fn_name := node.name.replace('.', '_')
t := g.new_tmp_var() + '_arg_expr_${name}_$i' // g.new_tmp_var() t := '_tt${g.tmp_count2}_arg_expr_${fn_name}_$i'
g.called_fn_name = name g.called_fn_name = name
/*
g.write('string $t = ')
g.expr(arg.expr)
g.writeln('; // to free $i ')
*/
str_expr := g.write_expr_to_string(arg.expr) str_expr := g.write_expr_to_string(arg.expr)
g.insert_before_stmt('string $t = $str_expr; // new. to free $i ') // g.insert_before_stmt('string $t = $str_expr; // new. to free $i ')
g.tmp_arg_vars_to_free << t // i cur_line = g.go_before_stmt(0)
// println('cur line ="$cur_line"')
g.writeln('string $t = $str_expr; // new. to free $i ')
} }
} }
// Handle `print(x)` // Handle `print(x)`
@ -602,31 +607,48 @@ fn (mut g Gen) fn_call(node ast.CallExpr) {
} else if g.pref.is_debug && node.name == 'panic' { } else if g.pref.is_debug && node.name == 'panic' {
paline, pafile, pamod, pafn := g.panic_debug_info(node.pos) paline, pafile, pamod, pafn := g.panic_debug_info(node.pos)
g.write('panic_debug($paline, tos3("$pafile"), tos3("$pamod"), tos3("$pafn"), ') g.write('panic_debug($paline, tos3("$pafile"), tos3("$pamod"), tos3("$pafn"), ')
g.call_args(node.args, node.expected_arg_types) // g.call_args(node.args, node.expected_arg_types) // , [])
g.call_args(node)
g.write(')') g.write(')')
} else { } else {
// Simple function call
if free_tmp_arg_vars {
// g.writeln(';')
g.write(cur_line + ' /* cur line*/')
}
g.write('${g.get_ternary_name(name)}(') g.write('${g.get_ternary_name(name)}(')
if g.is_json_fn { if g.is_json_fn {
g.write(json_obj) g.write(json_obj)
} else { } else {
g.call_args(node.args, node.expected_arg_types) // g.call_args(node.args, node.expected_arg_types) // , tmp_arg_vars_to_free)
g.call_args(node)
} }
g.write(')') g.write(')')
} }
g.is_c_call = false g.is_c_call = false
g.is_json_fn = false g.is_json_fn = false
if free_tmp_arg_vars && g.tmp_arg_vars_to_free.len > 0 { if free_tmp_arg_vars { // && tmp_arg_vars_to_free.len > 0 {
// g.writeln(';')
// g.write(cur_line + ' /* cur line*/')
// g.write(tmp)
// Now free the tmp arg vars right after the function call // Now free the tmp arg vars right after the function call
g.writeln(';') g.writeln(';')
for tmp in g.tmp_arg_vars_to_free { for i, arg in node.args {
g.writeln('string_free(&$tmp);') if arg.is_tmp_autofree {
fn_name := node.name.replace('.', '_')
tmp := '_tt${g.tmp_count2}_arg_expr_${fn_name}_$i'
g.writeln('string_free(&$tmp);')
}
} }
g.writeln('') g.writeln('')
g.tmp_arg_vars_to_free.clear()
} }
} }
fn (mut g Gen) call_args(args []ast.CallArg, expected_types []table.Type) { // fn (mut g Gen) call_args(args []ast.CallArg, expected_types []table.Type, tmp_arg_vars_to_free []string) {
// fn (mut g Gen) call_args(args []ast.CallArg, expected_types []table.Type) {
fn (mut g Gen) call_args(node ast.CallExpr) {
args := if g.is_js_call { node.args[1..] } else { node.args }
expected_types := node.expected_arg_types
is_variadic := expected_types.len > 0 && expected_types[expected_types.len - 1].has_flag(.variadic) is_variadic := expected_types.len > 0 && expected_types[expected_types.len - 1].has_flag(.variadic)
is_forwarding_varg := args.len > 0 && args[args.len - 1].typ.has_flag(.variadic) is_forwarding_varg := args.len > 0 && args[args.len - 1].typ.has_flag(.variadic)
gen_vargs := is_variadic && !is_forwarding_varg gen_vargs := is_variadic && !is_forwarding_varg
@ -635,7 +657,8 @@ fn (mut g Gen) call_args(args []ast.CallArg, expected_types []table.Type) {
break break
} }
use_tmp_var_autofree := g.autofree && g.pref.experimental && arg.typ == table.string_type && use_tmp_var_autofree := g.autofree && g.pref.experimental && arg.typ == table.string_type &&
g.tmp_arg_vars_to_free.len > 0 arg.is_tmp_autofree
// g.write('/* af=$arg.is_tmp_autofree */')
mut is_interface := false mut is_interface := false
// some c fn definitions dont have args (cfns.v) or are not updated in checker // some c fn definitions dont have args (cfns.v) or are not updated in checker
// when these are fixed we wont need this check // when these are fixed we wont need this check
@ -654,14 +677,14 @@ fn (mut g Gen) call_args(args []ast.CallArg, expected_types []table.Type) {
if is_interface { if is_interface {
g.expr(arg.expr) g.expr(arg.expr)
} else if use_tmp_var_autofree { } else if use_tmp_var_autofree {
for name in g.tmp_arg_vars_to_free { if arg.is_tmp_autofree {
// We saved expressions in temp variables so that they can be freed later. // We saved expressions in temp variables so that they can be freed later.
// `foo(str + str2) => x := str + str2; foo(x); x.free()` // `foo(str + str2) => x := str + str2; foo(x); x.free()`
// g.write('_arg_expr_${g.called_fn_name}_$i') // g.write('_arg_expr_${g.called_fn_name}_$i')
// Use these variables here. // Use these variables here.
if name.contains('_$i') { fn_name := node.name.replace('.', '_')
g.write(name) name := '_tt${g.tmp_count2}_arg_expr_${fn_name}_$i'
} g.write('/*af arg*/' + name)
} }
} else { } else {
g.ref_or_deref_arg(arg, expected_types[i]) g.ref_or_deref_arg(arg, expected_types[i])
@ -669,11 +692,9 @@ fn (mut g Gen) call_args(args []ast.CallArg, expected_types []table.Type) {
} else { } else {
if use_tmp_var_autofree { if use_tmp_var_autofree {
// TODO copypasta, move to an inline fn // TODO copypasta, move to an inline fn
for name in g.tmp_arg_vars_to_free { fn_name := node.name.replace('.', '_')
if name.contains('_$i') { name := '_tt${g.tmp_count2}_arg_expr_${fn_name}_$i'
g.write(name) g.write('/*af arg2*/' + name)
}
}
} else { } else {
g.expr(arg.expr) g.expr(arg.expr)
} }

View File

@ -40,7 +40,7 @@ pub fn (mut p Parser) call_expr(language table.Language, mod string) ast.CallExp
} }
} }
p.check(.lpar) p.check(.lpar)
args := p.call_args() mut args := p.call_args()
last_pos := p.tok.position() last_pos := p.tok.position()
p.check(.rpar) p.check(.rpar)
// ! in mutable methods // ! in mutable methods

View File

@ -15,12 +15,17 @@ fn foo() {
// nums.free() // this should result in a double free and a CI error // nums.free() // this should result in a double free and a CI error
} }
fn handle_strings(s, p string) { fn handle_strings(s, p string) int {
return 0
}
fn handle_int(n int) {
} }
fn str_tmp_expr() { fn str_tmp_expr() {
println('a' + 'b') // tmp expression result must be freed println('a' + 'b') // tmp expression result must be freed
handle_strings('c' + 'd', 'e' + 'f') handle_strings('c' + 'd', 'e' + 'f') // multiple tmp expressions must be freed
// handle_int(handle_strings('x' + 'y', 'f')) // exprs 2 levels deep must bee freed
} }
struct Foo { struct Foo {