Skip to content

slice return unexpect value when struct field modified after read #1608

@luoliwoshang

Description

@luoliwoshang

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

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions