Skip to content

Commit 2da732c

Browse files
committed
v3: address PR review batch 19 — heap escaped pointer-alias locals, unwrap fixed-array returns from static/import-qualified calls
Two PR-review fixes (three earlier comments were already resolved in batch 18; GitHub just re-anchored their line numbers): - transform.v: a value local whose address escapes (`p := &v` with `p` returned) was heap-copied eagerly at the alias, so a mutation between the alias and the return (`mut v := Box{x: 1}; p := &v; v.x = 2; return p`) was lost — the caller saw stale data (x == 1). Move the local itself to the heap at its declaration (its type becomes `&T`; a struct literal becomes `&T{..}`, any other init is copied into a temp and memdup'd) and make the alias `p := v`, so writes through `v` are visible to the caller, matching V's auto-heap semantics (V1 returns 2). cgen already handles pointer-typed locals. - stmt.v/cleanc.v: the fixed-array-return unwrap (`(call()).ret_arr`) relied on declared_call_return_type, whose selector path only handled receiver methods. A static method (`Type.make()`) or import-qualified function (`mod.make()`) returning `[N]T` emits a `_v_ret_*` wrapper that was never unwrapped, so the assignment/index saw the wrapper struct (invalid C). Resolve such selector calls the same way gen_call does (static_method_fn_name / import_alias_module) and read the declared return type. declared_call_return_type now takes the call node id. Adds regression tests for both.
1 parent 18bbe59 commit 2da732c

4 files changed

Lines changed: 135 additions & 10 deletions

File tree

vlib/v3/gen/c/cleanc.v

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1768,7 +1768,7 @@ fn (mut g FlatGen) gen_expr(id flat.NodeId) {
17681768
// A call to a fixed-array-returning function yields the wrapper struct;
17691769
// unwrap `.ret_arr` so the result behaves as the array value everywhere
17701770
// (indexing, arg passing, memcpy into a destination).
1771-
ret_t := g.declared_call_return_type(node)
1771+
ret_t := g.declared_call_return_type(id)
17721772
if ret_t is types.ArrayFixed && g.tc.c_type(ret_t) in g.fixed_array_ret_wrappers {
17731773
g.write('(')
17741774
g.gen_call(id, node)

vlib/v3/gen/c/stmt.v

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ fn (mut g FlatGen) gen_node(id flat.NodeId) {
261261
raw_expr_type := g.tc.resolve_type(ret_id)
262262
expr_type := g.usable_expr_type(ret_id)
263263
call_ret_type := g.local_fn_call_return_type(ret_id, ret_node)
264-
decl_ret_type := g.declared_call_return_type(ret_node)
264+
decl_ret_type := g.declared_call_return_type(ret_id)
265265
if g.optional_result_matches_base(raw_expr_type, base)
266266
|| g.optional_result_matches_base(expr_type, base)
267267
|| g.optional_result_matches_base(call_ret_type, base)
@@ -567,7 +567,7 @@ fn (mut g FlatGen) return_expr_string(node flat.Node, ret_id flat.NodeId, ret_no
567567
raw_expr_type := g.tc.resolve_type(ret_id)
568568
expr_type := g.usable_expr_type(ret_id)
569569
call_ret_type := g.local_fn_call_return_type(ret_id, ret_node)
570-
decl_ret_type := g.declared_call_return_type(ret_node)
570+
decl_ret_type := g.declared_call_return_type(ret_id)
571571
if g.optional_result_matches_base(raw_expr_type, base)
572572
|| g.optional_result_matches_base(expr_type, base)
573573
|| g.optional_result_matches_base(call_ret_type, base)
@@ -673,7 +673,11 @@ fn (g &FlatGen) local_fn_call_return_type(call_id flat.NodeId, call_node flat.No
673673
// becomes `?int`), which makes the optional C type name appear to differ from
674674
// the callee's signature. The declared type read from `fn_ret_types`/the fn decl
675675
// keeps the alias, so propagating `return call()` is recognised as valid.
676-
fn (g &FlatGen) declared_call_return_type(call_node flat.Node) types.Type {
676+
fn (g &FlatGen) declared_call_return_type(call_id flat.NodeId) types.Type {
677+
if int(call_id) < 0 {
678+
return types.Type(types.void_)
679+
}
680+
call_node := g.a.nodes[int(call_id)]
677681
if call_node.kind != .call || call_node.children_count == 0 {
678682
return types.Type(types.void_)
679683
}
@@ -716,6 +720,26 @@ fn (g &FlatGen) selector_call_return_type(fn_node flat.Node) ?types.Type {
716720
return none
717721
}
718722
base_id := g.a.child(&fn_node, 0)
723+
base_node := g.a.nodes[int(base_id)]
724+
// A selector whose base names a type or an imported module is not a receiver method but a
725+
// static method (`Type.make()`) or import-qualified function (`mod.make()`); the base ident
726+
// has no value type, so resolve it the same way gen_call does and read the declared return
727+
// type. Without this a fixed-array such call's `_v_ret_*` wrapper is never unwrapped.
728+
if base_node.kind == .ident && base_node.value.len > 0 {
729+
base_is_local := g.tc.cur_scope.lookup(base_node.value) or { types.Type(types.void_) } !is types.Void
730+
if !base_is_local {
731+
if static_fn := g.static_method_fn_name(base_node.value, fn_node.value) {
732+
if ret := g.tc.fn_ret_types[static_fn] {
733+
return ret
734+
}
735+
}
736+
if mod := g.import_alias_module(base_node.value) {
737+
if ret := g.tc.fn_ret_types['${mod}.${fn_node.value}'] {
738+
return ret
739+
}
740+
}
741+
}
742+
}
719743
base_type := g.tc.resolve_type(base_id)
720744
clean_type := types.unwrap_pointer(base_type)
721745
mut receiver_name := clean_type.name()

vlib/v3/tests/type_checker_errors_test.v

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,3 +778,19 @@ fn test_pr_review_codegen_batch_eighteen() {
778778
"fn work() [0x10]u8 {\n\tmut r := [0x10]u8{}\n\tr[0] = 42\n\tr[15] = 7\n\treturn r\n}\nfn main() {\n\tmut threads := []thread [0x10]u8{}\n\tthreads << spawn work()\n\tresults := threads.wait()\n\tprintln(int_str(results[0][0]) + ',' + int_str(results[0][15]))\n}\n")
779779
assert thread_fixed == '42,7'
780780
}
781+
782+
fn test_pr_review_codegen_batch_nineteen() {
783+
v3_bin := build_v3()
784+
// A value local whose address escapes (`p := &v` with `p` returned) is moved to the heap at
785+
// its declaration, so a mutation between the alias and the return is observed by the caller —
786+
// matching V's auto-heap semantics. Copying eagerly at `p := &v` returned stale data (x == 1).
787+
escaped := run_good(v3_bin, 'good_escaped_pointer_alias_mutation',
788+
'struct Box {\nmut:\n\tx int\n}\nfn make() &Box {\n\tmut v := Box{\n\t\tx: 1\n\t}\n\tp := &v\n\tv.x = 2\n\treturn p\n}\nfn main() {\n\tb := make()\n\tprintln(int_str(b.x))\n}\n')
789+
assert escaped == '2'
790+
// A static-method call returning a fixed array (`Type.make() [N]T`) is lowered to a selector
791+
// whose base is a type, not a receiver value; its `_v_ret_*` wrapper must still be unwrapped to
792+
// `.ret_arr`, so the assignment/indexing below sees the array, not the wrapper struct.
793+
static_fixed := run_good(v3_bin, 'good_static_method_fixed_array_return',
794+
'struct Maker {}\nfn Maker.make() [3]int {\n\treturn [10, 20, 30]!\n}\nfn main() {\n\ta := Maker.make()\n\tprintln(int_str(a[0] + a[1] + a[2]))\n}\n')
795+
assert static_fixed == '60'
796+
}

vlib/v3/transform/transform.v

Lines changed: 91 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,14 @@ mut:
9696
// on return. Recomputed per function (structural pre-pass in transform_fn_body),
9797
// consumed when the `p := &v` decl is transformed (RHS rewritten to a heap copy).
9898
escaping_amp_ptrs map[string]bool
99+
// escaping_amp_sources holds the source locals `v` of such `p := &v` escapes — the
100+
// values whose address leaves the frame. The local itself is moved to the heap at its
101+
// declaration (its type becomes `&T`) so a mutation between `p := &v` and `return p`
102+
// is observed by the caller; copying eagerly at the alias would return stale data.
103+
escaping_amp_sources map[string]bool
104+
// heaped_amp_locals records which of those sources were actually moved to the heap, so
105+
// the `p := &v` alias emits `p = v` (the heap pointer) instead of a fresh memdup copy.
106+
heaped_amp_locals map[string]bool
99107
}
100108

101109
// AliasCache memoizes normalize_type_alias results. It lives on the heap so the
@@ -1029,24 +1037,72 @@ fn (t &Transformer) try_heap_escaping_amp(node flat.Node, rhs_id flat.NodeId) bo
10291037
if amp_node.kind != .ident {
10301038
return false
10311039
}
1040+
// The source local was moved to the heap at its declaration: the alias is now just that
1041+
// `&T` pointer (handled below), regardless of its rewritten pointer type.
1042+
if amp_node.value in t.heaped_amp_locals {
1043+
return true
1044+
}
10321045
local_type := t.node_type(amp_child)
10331046
return local_type.len > 0 && !local_type.starts_with('&') && !local_type.starts_with('[]')
10341047
&& !local_type.starts_with('map[') && !local_type.starts_with('?')
10351048
&& !local_type.starts_with('!')
10361049
}
10371050

10381051
// heap_escaping_amp_rhs rewrites `&v` into `(&T)memdup(&v, sizeof(T))`, a heap copy
1039-
// of the value local `v` so the escaping pointer outlives the stack frame.
1052+
// of the value local `v` so the escaping pointer outlives the stack frame. When `v` was
1053+
// itself moved to the heap at its declaration, the alias is simply that pointer — copying
1054+
// would resurrect the stale-mutation bug the move avoids.
10401055
fn (mut t Transformer) heap_escaping_amp_rhs(rhs_id flat.NodeId) flat.NodeId {
10411056
rhs := t.a.nodes[int(rhs_id)]
10421057
amp_child := t.a.child(&rhs, 0)
1058+
amp_node := t.a.nodes[int(amp_child)]
1059+
if amp_node.kind == .ident && amp_node.value in t.heaped_amp_locals {
1060+
return t.transform_expr(amp_child)
1061+
}
10431062
local_type := t.node_type(amp_child)
10441063
addr := t.make_prefix(.amp, t.transform_expr(amp_child))
10451064
size := t.make_sizeof_type(local_type)
10461065
dup := t.make_call_typed('memdup', arr2(addr, size), 'voidptr')
10471066
return t.make_cast('&${local_type}', dup, '&${local_type}')
10481067
}
10491068

1069+
// heapable_value_type reports whether a local of this declared type can be moved to the heap
1070+
// as a `&T` — a plain value type, not an already-reference / container / optional type (those
1071+
// either carry their own indirection or are not addressable as a single `T`).
1072+
fn (t &Transformer) heapable_value_type(typ string) bool {
1073+
return typ.len > 0 && !typ.starts_with('&') && !typ.starts_with('[]')
1074+
&& !typ.starts_with('map[') && !typ.starts_with('?') && !typ.starts_with('!')
1075+
&& !typ.starts_with('[') && typ != 'unknown' && typ != 'void'
1076+
}
1077+
1078+
// heap_escaping_source_decl rewrites `mut v := <init>` (where `&v` escapes) into a heap
1079+
// allocation so `v` is a `&T` to a heap object. A struct literal becomes `&T{..}` (the cgen
1080+
// memdup's it); any other initializer is copied into a stack temp and memdup'd. Subsequent
1081+
// `v.field = ..` writes then mutate the heap object the returned pointer alias also sees.
1082+
fn (mut t Transformer) heap_escaping_source_decl(node flat.Node, var_name string, elem_typ string) []flat.NodeId {
1083+
rhs_id := t.a.child(&node, 1)
1084+
rhs := t.a.nodes[int(rhs_id)]
1085+
ptr_typ := '&${elem_typ}'
1086+
mut stmts := []flat.NodeId{}
1087+
transformed_init := t.transform_expr(rhs_id)
1088+
// Statements lifted out while transforming the initializer must precede the heap decl.
1089+
t.drain_pending(mut stmts)
1090+
mut heap_rhs := flat.NodeId(0)
1091+
if rhs.kind == .struct_init {
1092+
heap_rhs = t.make_prefix(.amp, transformed_init)
1093+
} else {
1094+
tmp := t.new_temp('esc')
1095+
stmts << t.make_decl_assign_typed(tmp, transformed_init, elem_typ)
1096+
addr := t.make_prefix(.amp, t.make_ident(tmp))
1097+
size := t.make_sizeof_type(elem_typ)
1098+
dup := t.make_call_typed('memdup', arr2(addr, size), 'voidptr')
1099+
heap_rhs = t.make_cast(ptr_typ, dup, ptr_typ)
1100+
}
1101+
t.heaped_amp_locals[var_name] = true
1102+
stmts << t.make_decl_assign_typed(var_name, heap_rhs, ptr_typ)
1103+
return stmts
1104+
}
1105+
10501106
// mark_escaping_amp_ptrs runs a structural pre-pass over a function body to find
10511107
// `p := &v` declarations whose pointer `p` is later returned. Such a `v` is a local
10521108
// value whose address escapes, so it must be heap-copied (V auto-heaps it); the
@@ -1055,22 +1111,29 @@ fn (mut t Transformer) heap_escaping_amp_rhs(rhs_id flat.NodeId) flat.NodeId {
10551111
// at rewrite time when `v`'s type is known.
10561112
fn (mut t Transformer) mark_escaping_amp_ptrs(body_ids []flat.NodeId) {
10571113
t.escaping_amp_ptrs = map[string]bool{}
1114+
t.escaping_amp_sources = map[string]bool{}
1115+
t.heaped_amp_locals = map[string]bool{}
10581116
mut amp_ptrs := map[string]bool{}
1117+
mut amp_sources := map[string]string{} // pointer `p` -> source local `v`
10591118
mut returned := map[string]bool{}
10601119
for id in body_ids {
1061-
t.scan_escape_pass(id, mut amp_ptrs, mut returned)
1120+
t.scan_escape_pass(id, mut amp_ptrs, mut amp_sources, mut returned)
10621121
}
10631122
for name, _ in amp_ptrs {
10641123
if name in returned {
10651124
t.escaping_amp_ptrs[name] = true
1125+
if src := amp_sources[name] {
1126+
t.escaping_amp_sources[src] = true
1127+
}
10661128
}
10671129
}
10681130
}
10691131

10701132
// scan_escape_pass recursively collects, in a function-body subtree, (a) the LHS
1071-
// names of `p := &ident` declarations into `amp_ptrs`, and (b) every ident name
1072-
// appearing inside a return statement into `returned`.
1073-
fn (mut t Transformer) scan_escape_pass(id flat.NodeId, mut amp_ptrs map[string]bool, mut returned map[string]bool) {
1133+
// names of `p := &ident` declarations into `amp_ptrs` (and the source `ident` into
1134+
// `amp_sources[p]`), and (b) every ident name appearing inside a return statement
1135+
// into `returned`.
1136+
fn (mut t Transformer) scan_escape_pass(id flat.NodeId, mut amp_ptrs map[string]bool, mut amp_sources map[string]string, mut returned map[string]bool) {
10741137
if int(id) < 0 || int(id) >= t.a.nodes.len {
10751138
return
10761139
}
@@ -1083,6 +1146,7 @@ fn (mut t Transformer) scan_escape_pass(id flat.NodeId, mut amp_ptrs map[string]
10831146
amp_child := t.a.nodes[int(t.a.child(&rhs, 0))]
10841147
if amp_child.kind == .ident {
10851148
amp_ptrs[lhs.value] = true
1149+
amp_sources[lhs.value] = amp_child.value
10861150
}
10871151
}
10881152
}
@@ -1092,7 +1156,7 @@ fn (mut t Transformer) scan_escape_pass(id flat.NodeId, mut amp_ptrs map[string]
10921156
}
10931157
}
10941158
for i in 0 .. node.children_count {
1095-
t.scan_escape_pass(t.a.child(&node, i), mut amp_ptrs, mut returned)
1159+
t.scan_escape_pass(t.a.child(&node, i), mut amp_ptrs, mut amp_sources, mut returned)
10961160
}
10971161
}
10981162

@@ -2687,13 +2751,34 @@ fn (mut t Transformer) transform_decl_assign_stmt(id flat.NodeId, node flat.Node
26872751
}
26882752
}
26892753
}
2754+
// A value local whose address escapes (`p := &v` with `p` returned) is moved to the heap
2755+
// at its own declaration so writes after the alias are visible to the caller. Must run
2756+
// before the `p := &v` alias is transformed (the source is declared first).
2757+
if node.children_count == 2 {
2758+
src := t.a.child_node(&node, 0)
2759+
if src.kind == .ident && src.value in t.escaping_amp_sources
2760+
&& src.value !in t.heaped_amp_locals && t.heapable_value_type(inferred_typ) {
2761+
return t.heap_escaping_source_decl(node, src.value, inferred_typ)
2762+
}
2763+
}
26902764
mut new_children := []flat.NodeId{cap: int(node.children_count)}
26912765
for i in 0 .. node.children_count {
26922766
child_id := t.a.child(&node, i)
26932767
if i == 0 || (node.children_count > 2 && i > 1) {
26942768
new_children << t.transform_lvalue(child_id)
26952769
} else if node.children_count == 2 && t.try_heap_escaping_amp(node, child_id) {
26962770
new_children << t.heap_escaping_amp_rhs(child_id)
2771+
// When `v` was heap-moved it is already a `&T`, so `p := &v` is really `p := v`
2772+
// (a `&T`), not `&&T` as the literal `&v` would infer. Adopt the source's pointer
2773+
// type for `p` so its declaration and later uses are consistent.
2774+
amp := t.a.nodes[int(child_id)]
2775+
if amp.children_count > 0 {
2776+
amp_src := t.a.nodes[int(t.a.child(&amp, 0))]
2777+
if amp_src.kind == .ident && amp_src.value in t.heaped_amp_locals {
2778+
inferred_typ = t.var_type(amp_src.value)
2779+
t.set_var_type(t.a.nodes[int(t.a.child(&node, 0))].value, inferred_typ)
2780+
}
2781+
}
26972782
} else {
26982783
lhs_id := t.a.child(&node, 0)
26992784
lhs_type := if inferred_typ.len > 0 {

0 commit comments

Comments
 (0)