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.
10401055fn (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.
10561112fn (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