-
Notifications
You must be signed in to change notification settings - Fork 46
Description
Problem Description
When a function returns a single slice value, and the slice is read from a struct field, then that field is modified before the return, llgo incorrectly returns the modified value instead of the original value.
This affects all control flow statements with initialization (if, switch, type switch), as they internally use this pattern in gogen's clearBlockStmt() function.
Minimal Reproduction
package main
type T struct {
data []string
}
// ❌ Single return value - BROKEN in llgo
func a() []string {
var t = T{data: []string{"a", "b"}}
a := t.data // Read: ["a", "b"]
t.data = []string{"a", "b", "c"} // Modify field
return a // Expected: ["a", "b"], Actual: ["a", "b", "c"]
}
// ✅ Multiple return values - WORKS in llgo
func b() ([]string, bool) {
var t = T{data: []string{"a", "b"}}
a := t.data // Read: ["a", "b"]
t.data = []string{"a", "b", "c"} // Modify field
return a, true // Correctly returns ["a", "b"]
}
func main() {
println(a()) // llgo: [3/3] ❌ Wrong length!
println(b()) // llgo: [2/2] ✅ Correct
}Expected Behavior
Standard Go output:
[2/2]0xc0000... # func a() returns length 2
[2/2]0xc0000... # func b() returns length 2
Actual Behavior (llgo)
llgo output:
[3/3]0xc0000... # func a() returns length 3 ❌
[2/2]0xc0000... # func b() returns length 2 ✅
Root Cause Analysis
LLVM IR shows the issue
The generated IR for single-return-value function appears correct, but at runtime the return value gets overwritten:
define %Slice @"...a"() {
%9 = load %Slice, ptr %8, align 8 ; Load original [a, b]
%16 = insertvalue %Slice ..., i64 3, 2 ; Create new [a, b, c]
store %Slice %16, ptr %17, align 8 ; Store new value
ret %Slice %9 ; Return %9, but it contains [a, b, c]!
}Why multi-value return works
The multi-value return uses insertvalue to construct a struct, which creates a copy of the slice:
define { %Slice, i1 } @"...b"() {
%9 = load %Slice, ptr %8, align 8
store %Slice %16, ptr %17, align 8
%18 = insertvalue { %Slice, i1 } undef, %Slice %9, 0 ; ← Creates copy!
ret { %Slice, i1 } %18
}Impact
🔴 Critical Bug
- Affects all slice return values following the "read-modify-return" pattern
- Silent data corruption (no crash, just wrong data)
- Affects basic Go control flow syntax (if/switch/typeswitch with init statements)
- Breaks gogen package's
clearBlockStmt()function
Affected Code Pattern
func buggy() []T {
x := struct.field // 1. Read struct field
struct.field = y // 2. Modify that field
return x // 3. Return ❌ Returns y instead of x
}Test Matrix
| Scenario | Standard Go | llgo |
|---|---|---|
| Local variable return | ✅ | ✅ |
| Direct field return | ✅ | ✅ |
| Read→Modify→Return (single value) | ✅ | ❌ |
| Read→Modify→Return (multi value) | ✅ | ✅ |
Workarounds
Option 1: Add dummy return value
func fixed() ([]T, bool) {
x := struct.field
struct.field = y
return x, true // ✅ Works
}Option 2: Explicit copy
func fixed() []T {
x := append([]T(nil), struct.field...)
struct.field = y
return x // ✅ Works
}Environment
- llgo version: latest main branch
- Related issue: Bug: TypeSwitch initialization statement missing when running under llgo when use gogen #1604 (TypeSwitch initialization lost)
Investigation Details
Full investigation with 13 test cases and LLVM IR analysis available at:
.issue/1604-typeswitch-init-missing/FINAL-ROOT-CAUSE.md
The issue appears to be in llgo's slice return value ABI or memory allocation strategy, where single-value slice returns share memory with the source struct field, causing the return value to be overwritten by subsequent modifications.