// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package ssa

import (
	"cmd/compile/internal/types"
	"fmt"
	"strconv"
	"testing"
)

func TestFuseEliminatesOneBranch(t *testing.T) {
	c := testConfig(t)
	ptrType := c.config.Types.BytePtr
	fun := c.Fun("entry",
		Bloc("entry",
			Valu("mem", OpInitMem, types.TypeMem, 0, nil),
			Valu("sb", OpSB, c.config.Types.Uintptr, 0, nil),
			Goto("checkPtr")),
		Bloc("checkPtr",
			Valu("ptr1", OpLoad, ptrType, 0, nil, "sb", "mem"),
			Valu("nilptr", OpConstNil, ptrType, 0, nil),
			Valu("bool1", OpNeqPtr, c.config.Types.Bool, 0, nil, "ptr1", "nilptr"),
			If("bool1", "then", "exit")),
		Bloc("then",
			Goto("exit")),
		Bloc("exit",
			Exit("mem")))

	CheckFunc(fun.f)
	fuseLate(fun.f)

	for _, b := range fun.f.Blocks {
		if b == fun.blocks["then"] && b.Kind != BlockInvalid {
			t.Errorf("then was not eliminated, but should have")
		}
	}
}

func TestFuseEliminatesBothBranches(t *testing.T) {
	c := testConfig(t)
	ptrType := c.config.Types.BytePtr
	fun := c.Fun("entry",
		Bloc("entry",
			Valu("mem", OpInitMem, types.TypeMem, 0, nil),
			Valu("sb", OpSB, c.config.Types.Uintptr, 0, nil),
			Goto("checkPtr")),
		Bloc("checkPtr",
			Valu("ptr1", OpLoad, ptrType, 0, nil, "sb", "mem"),
			Valu("nilptr", OpConstNil, ptrType, 0, nil),
			Valu("bool1", OpNeqPtr, c.config.Types.Bool, 0, nil, "ptr1", "nilptr"),
			If("bool1", "then", "else")),
		Bloc("then",
			Goto("exit")),
		Bloc("else",
			Goto("exit")),
		Bloc("exit",
			Exit("mem")))

	CheckFunc(fun.f)
	fuseLate(fun.f)

	for _, b := range fun.f.Blocks {
		if b == fun.blocks["then"] && b.Kind != BlockInvalid {
			t.Errorf("then was not eliminated, but should have")
		}
		if b == fun.blocks["else"] && b.Kind != BlockInvalid {
			t.Errorf("else was not eliminated, but should have")
		}
	}
}

func TestFuseHandlesPhis(t *testing.T) {
	c := testConfig(t)
	ptrType := c.config.Types.BytePtr
	fun := c.Fun("entry",
		Bloc("entry",
			Valu("mem", OpInitMem, types.TypeMem, 0, nil),
			Valu("sb", OpSB, c.config.Types.Uintptr, 0, nil),
			Goto("checkPtr")),
		Bloc("checkPtr",
			Valu("ptr1", OpLoad, ptrType, 0, nil, "sb", "mem"),
			Valu("nilptr", OpConstNil, ptrType, 0, nil),
			Valu("bool1", OpNeqPtr, c.config.Types.Bool, 0, nil, "ptr1", "nilptr"),
			If("bool1", "then", "else")),
		Bloc("then",
			Goto("exit")),
		Bloc("else",
			Goto("exit")),
		Bloc("exit",
			Valu("phi", OpPhi, ptrType, 0, nil, "ptr1", "ptr1"),
			Exit("mem")))

	CheckFunc(fun.f)
	fuseLate(fun.f)

	for _, b := range fun.f.Blocks {
		if b == fun.blocks["then"] && b.Kind != BlockInvalid {
			t.Errorf("then was not eliminated, but should have")
		}
		if b == fun.blocks["else"] && b.Kind != BlockInvalid {
			t.Errorf("else was not eliminated, but should have")
		}
	}
}

func TestFuseEliminatesEmptyBlocks(t *testing.T) {
	c := testConfig(t)
	// Case 1, plain type empty blocks z0 ~ z3 will be eliminated.
	//     entry
	//       |
	//      z0
	//       |
	//      z1
	//       |
	//      z2
	//       |
	//      z3
	//       |
	//     exit
	fun := c.Fun("entry",
		Bloc("entry",
			Valu("mem", OpInitMem, types.TypeMem, 0, nil),
			Valu("sb", OpSB, c.config.Types.Uintptr, 0, nil),
			Goto("z0")),
		Bloc("z1",
			Goto("z2")),
		Bloc("z3",
			Goto("exit")),
		Bloc("z2",
			Goto("z3")),
		Bloc("z0",
			Goto("z1")),
		Bloc("exit",
			Exit("mem"),
		))

	CheckFunc(fun.f)
	fuseLate(fun.f)

	for k, b := range fun.blocks {
		if k[:1] == "z" && b.Kind != BlockInvalid {
			t.Errorf("case1 %s was not eliminated, but should have", k)
		}
	}

	// Case 2, empty blocks with If branch, z0 and z1 will be eliminated.
	//     entry
	//     /  \
	//    z0  z1
	//     \  /
	//     exit
	fun = c.Fun("entry",
		Bloc("entry",
			Valu("mem", OpInitMem, types.TypeMem, 0, nil),
			Valu("c", OpArg, c.config.Types.Bool, 0, nil),
			If("c", "z0", "z1")),
		Bloc("z0",
			Goto("exit")),
		Bloc("z1",
			Goto("exit")),
		Bloc("exit",
			Exit("mem"),
		))

	CheckFunc(fun.f)
	fuseLate(fun.f)

	for k, b := range fun.blocks {
		if k[:1] == "z" && b.Kind != BlockInvalid {
			t.Errorf("case2 %s was not eliminated, but should have", k)
		}
	}

	// Case 3, empty blocks with multiple predecessors, z0 and z1 will be eliminated.
	//     entry
	//      |  \
	//      |  b0
	//      | /  \
	//      z0   z1
	//       \   /
	//       exit
	fun = c.Fun("entry",
		Bloc("entry",
			Valu("mem", OpInitMem, types.TypeMem, 0, nil),
			Valu("c1", OpArg, c.config.Types.Bool, 0, nil),
			If("c1", "b0", "z0")),
		Bloc("b0",
			Valu("c2", OpArg, c.config.Types.Bool, 0, nil),
			If("c2", "z1", "z0")),
		Bloc("z0",
			Goto("exit")),
		Bloc("z1",
			Goto("exit")),
		Bloc("exit",
			Exit("mem"),
		))

	CheckFunc(fun.f)
	fuseLate(fun.f)

	for k, b := range fun.blocks {
		if k[:1] == "z" && b.Kind != BlockInvalid {
			t.Errorf("case3 %s was not eliminated, but should have", k)
		}
	}
}

func TestFuseSideEffects(t *testing.T) {
	c := testConfig(t)
	// Case1, test that we don't fuse branches that have side effects but
	// have no use (e.g. followed by infinite loop).
	// See issue #36005.
	fun := c.Fun("entry",
		Bloc("entry",
			Valu("mem", OpInitMem, types.TypeMem, 0, nil),
			Valu("b", OpArg, c.config.Types.Bool, 0, nil),
			If("b", "then", "else")),
		Bloc("then",
			Valu("call1", OpStaticCall, types.TypeMem, 0, AuxCallLSym("_"), "mem"),
			Goto("empty")),
		Bloc("else",
			Valu("call2", OpStaticCall, types.TypeMem, 0, AuxCallLSym("_"), "mem"),
			Goto("empty")),
		Bloc("empty",
			Goto("loop")),
		Bloc("loop",
			Goto("loop")))

	CheckFunc(fun.f)
	fuseLate(fun.f)

	for _, b := range fun.f.Blocks {
		if b == fun.blocks["then"] && b.Kind == BlockInvalid {
			t.Errorf("then is eliminated, but should not")
		}
		if b == fun.blocks["else"] && b.Kind == BlockInvalid {
			t.Errorf("else is eliminated, but should not")
		}
	}

	// Case2, z0 contains a value that has side effect, z0 shouldn't be eliminated.
	//     entry
	//      | \
	//      |  z0
	//      | /
	//     exit
	fun = c.Fun("entry",
		Bloc("entry",
			Valu("mem", OpInitMem, types.TypeMem, 0, nil),
			Valu("c1", OpArg, c.config.Types.Bool, 0, nil),
			Valu("p", OpArg, c.config.Types.IntPtr, 0, nil),
			If("c1", "z0", "exit")),
		Bloc("z0",
			Valu("nilcheck", OpNilCheck, types.TypeVoid, 0, nil, "p", "mem"),
			Goto("exit")),
		Bloc("exit",
			Exit("mem"),
		))
	CheckFunc(fun.f)
	fuseLate(fun.f)
	z0, ok := fun.blocks["z0"]
	if !ok || z0.Kind == BlockInvalid {
		t.Errorf("case2 z0 is eliminated, but should not")
	}
}

func BenchmarkFuse(b *testing.B) {
	for _, n := range [...]int{1, 10, 100, 1000, 10000} {
		b.Run(strconv.Itoa(n), func(b *testing.B) {
			c := testConfig(b)

			blocks := make([]bloc, 0, 2*n+3)
			blocks = append(blocks,
				Bloc("entry",
					Valu("mem", OpInitMem, types.TypeMem, 0, nil),
					Valu("cond", OpArg, c.config.Types.Bool, 0, nil),
					Valu("x", OpArg, c.config.Types.Int64, 0, nil),
					Goto("exit")))

			phiArgs := make([]string, 0, 2*n)
			for i := 0; i < n; i++ {
				cname := fmt.Sprintf("c%d", i)
				blocks = append(blocks,
					Bloc(fmt.Sprintf("b%d", i), If("cond", cname, "merge")),
					Bloc(cname, Goto("merge")))
				phiArgs = append(phiArgs, "x", "x")
			}
			blocks = append(blocks,
				Bloc("merge",
					Valu("phi", OpPhi, types.TypeMem, 0, nil, phiArgs...),
					Goto("exit")),
				Bloc("exit",
					Exit("mem")))

			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				fun := c.Fun("entry", blocks...)
				fuseLate(fun.f)
			}
		})
	}
}
