Skip to content

Commit 5f5f311

Browse files
committed
v3: address PR review batch 25 — escape-mark only returned pointers, optional fixed-array returns through defers
The escape prepass (mark_escaping_amp_ptrs) collected every ident in a return expression, so a pointer that merely appears in a consuming expression was treated as escaping. For `return p == p && v == 1`, that heap-moved `v` even though only a bool is returned, and later codegen read `v` as an int* in the integer comparison (wrong result). Replace collect_subtree_idents with collect_return_escape_idents, which stops at operators that consume their operands into a fresh value (infix, postfix, is/in, and any non-& prefix such as deref *p), so only pointers in a real escape position (the returned value or a member of a returned aggregate) mark their source local. This also stops over-marking `return *p`. An optional fixed-array function (?[N]T) with a pending defer routes through gen_return_with_defers -> return_expr_string, which lacked the fixed-array special case the direct path has. The optional's .value is a fixed-array member that can't be set via a compound literal, so the deferred path emitted {.ok = false}, dropping the array. Mirror the direct path's temp + memcpy form (new fixed_array_copy_source_string capture helper). Adds regression coverage (batch twentysix).
1 parent 8ff74f6 commit 5f5f311

4 files changed

Lines changed: 77 additions & 7 deletions

File tree

vlib/v3/gen/c/cleanc.v

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -905,6 +905,21 @@ fn (mut g FlatGen) interface_value_to_string(id flat.NodeId, expected types.Type
905905
return result
906906
}
907907

908+
// fixed_array_copy_source_string captures gen_fixed_array_copy_source as a string, so a deferred
909+
// optional/fixed-array return can embed the memcpy source when saving the value into a temp.
910+
fn (mut g FlatGen) fixed_array_copy_source_string(value_id flat.NodeId, field_type types.Type) string {
911+
orig := g.sb
912+
orig_line_start := g.line_start
913+
g.sb = strings.new_builder(64)
914+
// Emit mid-statement (no leading indent), matching the direct return path.
915+
g.line_start = false
916+
g.gen_fixed_array_copy_source(value_id, field_type)
917+
result := g.sb.str()
918+
g.sb = orig
919+
g.line_start = orig_line_start
920+
return result
921+
}
922+
908923
// expr_to_string_with_expected_type converts expr to string with expected type data for c.
909924
fn (mut g FlatGen) expr_to_string_with_expected_type(id flat.NodeId, expected types.Type) string {
910925
orig := g.sb

vlib/v3/gen/c/stmt.v

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,13 @@ fn (mut g FlatGen) return_expr_string(node flat.Node, ret_id flat.NodeId, ret_no
564564
if base is types.Void {
565565
return '(${ct}){.ok = false}'
566566
}
567+
if base is types.ArrayFixed {
568+
// The optional's `.value` is a fixed-array member, which can't be set in a compound
569+
// literal; build via a temp + memcpy (mirrors the direct return path) so a deferred
570+
// return saves the array value instead of dropping it to `{.ok = false}`.
571+
src := g.fixed_array_copy_source_string(ret_id, base)
572+
return '({ ${ct} __opt = {.ok = true}; memcpy(__opt.value, ${src}, sizeof(__opt.value)); __opt; })'
573+
}
567574
raw_expr_type := g.tc.resolve_type(ret_id)
568575
expr_type := g.usable_expr_type(ret_id)
569576
call_ret_type := g.local_fn_call_return_type(ret_id, ret_node)

vlib/v3/tests/type_checker_errors_test.v

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -949,3 +949,27 @@ fn test_pr_review_codegen_batch_twentyfive() {
949949
"fn compound() int {\n\tmut v := 1\n\tp := &v\n\tv += 1\n\treturn *p\n}\nfn postfix() int {\n\tmut v := 5\n\tp := &v\n\tv++\n\tv++\n\treturn *p\n}\nfn main() {\n\tprintln(int_str(compound()) + ',' + int_str(postfix()))\n}\n")
950950
assert heap_compound == '2,7'
951951
}
952+
953+
fn test_pr_review_codegen_batch_twentysix() {
954+
v3_bin := build_v3()
955+
// The escape prepass must only heap-move a local whose address is *actually returned*. A
956+
// pointer that merely appears in a consuming expression (comparison/boolean here, or a deref)
957+
// does not escape, so its source local must stay a plain value — otherwise codegen reads it as
958+
// an `int*` in the integer comparison and the result is wrong. `return p == p && v == 1` and
959+
// `return *p` both keep `v` on the stack.
960+
no_escape := run_good(v3_bin, 'good_escape_only_returned_pointer',
961+
"fn check() bool {\n\tmut v := 1\n\tp := &v\n\treturn p == p && v == 1\n}\nfn deref() int {\n\tmut v := 3\n\tp := &v\n\tv = 8\n\treturn *p\n}\nfn main() {\n\tprintln(check().str() + ',' + int_str(deref()))\n}\n")
962+
assert no_escape == 'true,8'
963+
// A pointer that *is* returned (directly, and inside a returned aggregate) must still heap-move
964+
// its source local, so the returned pointer observes mutations made after the address was taken.
965+
escapes := run_good(v3_bin, 'good_escape_returned_pointer_and_aggregate',
966+
"fn direct() &int {\n\tmut v := 10\n\tp := &v\n\tv += 5\n\treturn p\n}\nfn aggregate() []&int {\n\tmut v := 7\n\tp := &v\n\tv = 9\n\treturn [p]\n}\nfn main() {\n\tarr := aggregate()\n\tprintln(int_str(*direct()) + ',' + int_str(*arr[0]))\n}\n")
967+
assert escapes == '15,9'
968+
// An optional fixed-array return (`?[N]T`) with a pending `defer` routes through the deferred
969+
// return path; that path must apply the same temp + memcpy handling as the direct path (the
970+
// `.value` member is a fixed array and cannot be set via a compound literal), so the array value
971+
// is preserved instead of being dropped to `{.ok = false}`.
972+
defer_opt_arr := run_good(v3_bin, 'good_optional_fixed_array_return_with_defer',
973+
"fn f() ?[2]int {\n\tdefer {\n\t\t_ := 0\n\t}\n\treturn [1, 2]!\n}\nfn main() {\n\ta := f() or { [0, 0]! }\n\tprintln(int_str(a[0]) + ',' + int_str(a[1]))\n}\n")
974+
assert defer_opt_arr == '1,2'
975+
}

vlib/v3/transform/transform.v

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1178,7 +1178,7 @@ fn (mut t Transformer) scan_escape_pass(id flat.NodeId, mut amp_ptrs map[string]
11781178
}
11791179
if node.kind == .return_stmt {
11801180
for i in 0 .. node.children_count {
1181-
t.collect_subtree_idents(t.a.child(&node, i), mut returned)
1181+
t.collect_return_escape_idents(t.a.child(&node, i), mut returned)
11821182
}
11831183
}
11841184
for i in 0 .. node.children_count {
@@ -1187,18 +1187,42 @@ fn (mut t Transformer) scan_escape_pass(id flat.NodeId, mut amp_ptrs map[string]
11871187
}
11881188
}
11891189

1190-
// collect_subtree_idents gathers every ident name in a subtree (used to find which
1191-
// names flow into a return statement).
1192-
fn (mut t Transformer) collect_subtree_idents(id flat.NodeId, mut names map[string]bool) {
1190+
// collect_return_escape_idents gathers the idents in a return-expression subtree that occupy an
1191+
// actual escape position — the returned value itself, or a member of a returned aggregate
1192+
// (struct/array/map literal, multi-return). It deliberately stops at operators that consume their
1193+
// operands into a fresh value: infix (`==`, `&&`, arithmetic, …), postfix, `is`/`in`, and any
1194+
// non-`&` prefix (deref `*p`, `!x`, `-x`). That way a pointer that is merely compared or
1195+
// dereferenced in the return expression — e.g. `return p == p && v == 1` — is not mistaken for a
1196+
// pointer that escapes, so its source local is not needlessly heap-moved (which would also make
1197+
// later non-pointer uses of that local read through an `int*`).
1198+
fn (mut t Transformer) collect_return_escape_idents(id flat.NodeId, mut names map[string]bool) {
11931199
if int(id) < 0 || int(id) >= t.a.nodes.len {
11941200
return
11951201
}
11961202
node := t.a.nodes[int(id)]
1197-
if node.kind == .ident && node.value.len > 0 {
1198-
names[node.value] = true
1203+
match node.kind {
1204+
.ident {
1205+
if node.value.len > 0 {
1206+
names[node.value] = true
1207+
}
1208+
return
1209+
}
1210+
.infix, .postfix, .is_expr, .in_expr {
1211+
// These yield a new scalar/bool; their operands do not escape through the return.
1212+
return
1213+
}
1214+
.prefix {
1215+
// `&x` propagates an address (which may escape); any other prefix (`*x`, `!x`, `-x`)
1216+
// produces a fresh value, so its operand does not escape.
1217+
if node.op != .amp {
1218+
return
1219+
}
1220+
}
1221+
else {}
11991222
}
1223+
12001224
for i in 0 .. node.children_count {
1201-
t.collect_subtree_idents(t.a.child(&node, i), mut names)
1225+
t.collect_return_escape_idents(t.a.child(&node, i), mut names)
12021226
}
12031227
}
12041228

0 commit comments

Comments
 (0)