Skip to content

Commit 0228ae2

Browse files
luoliwoshangclaude
andcommitted
cabi: add regression test for issue goplus#1608
Add TestIssue1608_AttrPointerReturnNoMemcpy to verify CABI transformation uses store instead of memcpy for sret returns. The test creates IR where: 1. A slice is loaded from a struct field 2. The struct field is modified 3. The originally loaded slice is returned This ensures the fix prevents the memcpy optimization bug where the source address content could change between load and ret. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 7a2ab5a commit 0228ae2

File tree

1 file changed

+124
-0
lines changed

1 file changed

+124
-0
lines changed

internal/cabi/cabi_test.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,3 +248,127 @@ func sretAttribute(ctx llvm.Context, typ llvm.Type) llvm.Attribute {
248248
id := llvm.AttributeKindID("sret")
249249
return ctx.CreateTypeAttribute(id, typ)
250250
}
251+
252+
// TestIssue1608_AttrPointerReturnNoMemcpy is a regression test for
253+
// https://github.com/goplus/llgo/issues/1608
254+
//
255+
// When transforming functions with sret (structure return), the optimizer
256+
// previously used memcpy from the load source address. However, if the source
257+
// address content is modified between load and ret, the memcpy would copy the
258+
// wrong value. The fix is to always use store instead of memcpy.
259+
func TestIssue1608_AttrPointerReturnNoMemcpy(t *testing.T) {
260+
// Create test IR that mimics the bug scenario:
261+
// 1. Load a slice from struct field
262+
// 2. Modify the struct field
263+
// 3. Return the originally loaded slice
264+
testIR := `; ModuleID = 'test'
265+
source_filename = "test"
266+
267+
%Slice = type { ptr, i64, i64 }
268+
%T = type { %Slice }
269+
270+
define %Slice @testfunc() {
271+
entry:
272+
%0 = alloca %T, align 8
273+
%1 = getelementptr inbounds %T, ptr %0, i32 0, i32 0
274+
275+
; Store first slice {ptr, 2, 2}
276+
%2 = insertvalue %Slice undef, ptr null, 0
277+
%3 = insertvalue %Slice %2, i64 2, 1
278+
%4 = insertvalue %Slice %3, i64 2, 2
279+
store %Slice %4, ptr %1, align 8
280+
281+
; Load the slice (should return this value)
282+
%5 = getelementptr inbounds %T, ptr %0, i32 0, i32 0
283+
%6 = load %Slice, ptr %5, align 8
284+
285+
; Modify the source - THIS IS THE KEY!
286+
%7 = insertvalue %Slice undef, ptr null, 0
287+
%8 = insertvalue %Slice %7, i64 3, 1
288+
%9 = insertvalue %Slice %8, i64 3, 2
289+
%10 = getelementptr inbounds %T, ptr %0, i32 0, i32 0
290+
store %Slice %9, ptr %10, align 8
291+
292+
; Return the originally loaded value
293+
ret %Slice %6
294+
}
295+
`
296+
297+
// Expected IR after CABI transformation:
298+
// - Function signature changed to use sret
299+
// - Return value stored via sret pointer (NOT memcpy!)
300+
// - Returns void
301+
expectedFuncIR := `define void @testfunc(ptr sret(%Slice) %0) {
302+
entry:
303+
%1 = alloca %T, align 8
304+
%2 = getelementptr inbounds %T, ptr %1, i32 0, i32 0
305+
%3 = insertvalue %Slice undef, ptr null, 0
306+
%4 = insertvalue %Slice %3, i64 2, 1
307+
%5 = insertvalue %Slice %4, i64 2, 2
308+
store %Slice %5, ptr %2, align 8
309+
%6 = getelementptr inbounds %T, ptr %1, i32 0, i32 0
310+
%7 = load %Slice, ptr %6, align 8
311+
%8 = insertvalue %Slice undef, ptr null, 0
312+
%9 = insertvalue %Slice %8, i64 3, 1
313+
%10 = insertvalue %Slice %9, i64 3, 2
314+
%11 = getelementptr inbounds %T, ptr %1, i32 0, i32 0
315+
store %Slice %10, ptr %11, align 8
316+
store %Slice %7, ptr %0, align 8
317+
ret void
318+
}
319+
`
320+
321+
ctx := llvm.NewContext()
322+
defer ctx.Dispose()
323+
324+
// Write test IR to temporary file
325+
tmpfile := filepath.Join(t.TempDir(), "test.ll")
326+
if err := os.WriteFile(tmpfile, []byte(testIR), 0644); err != nil {
327+
t.Fatalf("Failed to write test IR: %v", err)
328+
}
329+
330+
// Parse the test IR
331+
buf, err := llvm.NewMemoryBufferFromFile(tmpfile)
332+
if err != nil {
333+
t.Fatalf("Failed to read test IR: %v", err)
334+
}
335+
336+
mod, err := ctx.ParseIR(buf)
337+
if err != nil {
338+
t.Fatalf("Failed to parse test IR: %v", err)
339+
}
340+
defer mod.Dispose()
341+
342+
// Get the function before transformation
343+
fn := mod.NamedFunction("testfunc")
344+
if fn.IsNil() {
345+
t.Fatal("Test function not found")
346+
}
347+
348+
// Build minimal config for CABI transformation
349+
conf, _ := buildConf(cabi.ModeAllFunc, "arm64")
350+
pkgs, err := build.Do([]string{"./_testdata/demo/demo.go"}, conf)
351+
if err != nil {
352+
t.Fatalf("Failed to build demo: %v", err)
353+
}
354+
355+
prog := pkgs[0].LPkg.Prog
356+
357+
// Apply CABI transformation with optimize=true
358+
// This tests the code path that previously had the memcpy bug
359+
tr := cabi.NewTransformer(prog, "", "", cabi.ModeAllFunc, true)
360+
tr.TransformModule("test", mod)
361+
362+
// Get transformed function IR
363+
transformedFn := mod.NamedFunction("testfunc")
364+
if transformedFn.IsNil() {
365+
t.Fatal("Transformed function not found")
366+
}
367+
actualFuncIR := strings.TrimSpace(transformedFn.String())
368+
expectedFuncIRTrimmed := strings.TrimSpace(expectedFuncIR)
369+
370+
// Compare IR
371+
if actualFuncIR != expectedFuncIRTrimmed {
372+
t.Errorf("Transformed IR mismatch!\n\nExpected:\n%s\n\nActual:\n%s", expectedFuncIRTrimmed, actualFuncIR)
373+
}
374+
}

0 commit comments

Comments
 (0)