// Copyright 2023 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 inline

// This file defines the analysis of callee effects.

import (
	"go/ast"
	"go/token"
	"go/types"
)

const (
	rinf = -1 //  R∞: arbitrary read from memory
	winf = -2 //  W∞: arbitrary write to memory (or unknown control)
)

// calleefx returns a list of parameter indices indicating the order
// in which parameters are first referenced during evaluation of the
// callee, relative both to each other and to other effects of the
// callee (if any), such as arbitrary reads (rinf) and arbitrary
// effects (winf), including unknown control flow. Each parameter
// that is referenced appears once in the list.
//
// For example, the effects list of this function:
//
//	func f(x, y, z int) int {
//	    return y + x + g() + z
//	}
//
// is [1 0 -2 2], indicating reads of y and x, followed by the unknown
// effects of the g() call, and finally the read of parameter z. This
// information is used during inlining to ascertain when it is safe
// for parameter references to be replaced by their corresponding
// argument expressions. Such substitutions are permitted only when
// they do not cause "write" operations (those with effects) to
// commute with "read" operations (those that have no effect but are
// not pure). Impure operations may be reordered with other impure
// operations, and pure operations may be reordered arbitrarily.
//
// The analysis ignores the effects of runtime panics, on the
// assumption that well-behaved programs shouldn't encounter them.
func calleefx(info *types.Info, body *ast.BlockStmt, paramInfos map[*types.Var]*paramInfo) []int {
	// This traversal analyzes the callee's statements (in syntax
	// form, though one could do better with SSA) to compute the
	// sequence of events of the following kinds:
	//
	// 1  read of a parameter variable.
	// 2. reads from other memory.
	// 3. writes to memory

	var effects []int // indices of parameters, or rinf/winf (-ve)
	seen := make(map[int]bool)
	effect := func(i int) {
		if !seen[i] {
			seen[i] = true
			effects = append(effects, i)
		}
	}

	// unknown is called for statements of unknown effects (or control).
	unknown := func() {
		effect(winf)

		// Ensure that all remaining parameters are "seen"
		// after we go into the unknown (unless they are
		// unreferenced by the function body). This lets us
		// not bother implementing the complete traversal into
		// control structures.
		//
		// TODO(adonovan): add them in a deterministic order.
		// (This is not a bug but determinism is good.)
		for _, pinfo := range paramInfos {
			if !pinfo.IsResult && len(pinfo.Refs) > 0 {
				effect(pinfo.Index)
			}
		}
	}

	var visitExpr func(n ast.Expr)
	var visitStmt func(n ast.Stmt) bool
	visitExpr = func(n ast.Expr) {
		switch n := n.(type) {
		case *ast.Ident:
			if v, ok := info.Uses[n].(*types.Var); ok && !v.IsField() {
				// Use of global?
				if v.Parent() == v.Pkg().Scope() {
					effect(rinf) // read global var
				}

				// Use of parameter?
				if pinfo, ok := paramInfos[v]; ok && !pinfo.IsResult {
					effect(pinfo.Index) // read parameter var
				}

				// Use of local variables is ok.
			}

		case *ast.BasicLit:
			// no effect

		case *ast.FuncLit:
			// A func literal has no read or write effect
			// until called, and (most) function calls are
			// considered to have arbitrary effects.
			// So, no effect.

		case *ast.CompositeLit:
			for _, elt := range n.Elts {
				visitExpr(elt) // note: visits KeyValueExpr
			}

		case *ast.ParenExpr:
			visitExpr(n.X)

		case *ast.SelectorExpr:
			if seln, ok := info.Selections[n]; ok {
				visitExpr(n.X)

				// See types.SelectionKind for background.
				switch seln.Kind() {
				case types.MethodExpr:
					// A method expression T.f acts like a
					// reference to a func decl,
					// so it doesn't read x until called.

				case types.MethodVal, types.FieldVal:
					// A field or method value selection x.f
					// reads x if the selection indirects a pointer.

					if indirectSelection(seln) {
						effect(rinf)
					}
				}
			} else {
				// qualified identifier: treat like unqualified
				visitExpr(n.Sel)
			}

		case *ast.IndexExpr:
			if tv := info.Types[n.Index]; tv.IsType() {
				// no effect (G[T] instantiation)
			} else {
				visitExpr(n.X)
				visitExpr(n.Index)
				switch tv.Type.Underlying().(type) {
				case *types.Slice, *types.Pointer: // []T, *[n]T (not string, [n]T)
					effect(rinf) // indirect read of slice/array element
				}
			}

		case *ast.IndexListExpr:
			// no effect (M[K,V] instantiation)

		case *ast.SliceExpr:
			visitExpr(n.X)
			visitExpr(n.Low)
			visitExpr(n.High)
			visitExpr(n.Max)

		case *ast.TypeAssertExpr:
			visitExpr(n.X)

		case *ast.CallExpr:
			if info.Types[n.Fun].IsType() {
				// conversion T(x)
				visitExpr(n.Args[0])
			} else {
				// call f(args)
				visitExpr(n.Fun)
				for i, arg := range n.Args {
					if i == 0 && info.Types[arg].IsType() {
						continue // new(T), make(T, n)
					}
					visitExpr(arg)
				}

				// The pure built-ins have no effects beyond
				// those of their operands (not even memory reads).
				// All other calls have unknown effects.
				if !callsPureBuiltin(info, n) {
					unknown() // arbitrary effects
				}
			}

		case *ast.StarExpr:
			visitExpr(n.X)
			effect(rinf) // *ptr load or store depends on state of heap

		case *ast.UnaryExpr: // + - ! ^ & ~ <-
			visitExpr(n.X)
			if n.Op == token.ARROW {
				unknown() // effect: channel receive
			}

		case *ast.BinaryExpr:
			visitExpr(n.X)
			visitExpr(n.Y)

		case *ast.KeyValueExpr:
			visitExpr(n.Key) // may be a struct field
			visitExpr(n.Value)

		case *ast.BadExpr:
			// no effect

		case nil:
			// optional subtree

		default:
			// type syntax: unreachable given traversal
			panic(n)
		}
	}

	// visitStmt's result indicates the continuation:
	// false for return, true for the next statement.
	//
	// We could treat return as an unknown, but this way
	// yields definite effects for simple sequences like
	// {S1; S2; return}, so unreferenced parameters are
	// not spuriously added to the effects list, and thus
	// not spuriously disqualified from elimination.
	visitStmt = func(n ast.Stmt) bool {
		switch n := n.(type) {
		case *ast.DeclStmt:
			decl := n.Decl.(*ast.GenDecl)
			for _, spec := range decl.Specs {
				switch spec := spec.(type) {
				case *ast.ValueSpec:
					for _, v := range spec.Values {
						visitExpr(v)
					}

				case *ast.TypeSpec:
					// no effect
				}
			}

		case *ast.LabeledStmt:
			return visitStmt(n.Stmt)

		case *ast.ExprStmt:
			visitExpr(n.X)

		case *ast.SendStmt:
			visitExpr(n.Chan)
			visitExpr(n.Value)
			unknown() // effect: channel send

		case *ast.IncDecStmt:
			visitExpr(n.X)
			unknown() // effect: variable increment

		case *ast.AssignStmt:
			for _, lhs := range n.Lhs {
				visitExpr(lhs)
			}
			for _, rhs := range n.Rhs {
				visitExpr(rhs)
			}
			for _, lhs := range n.Lhs {
				id, _ := lhs.(*ast.Ident)
				if id != nil && id.Name == "_" {
					continue // blank assign has no effect
				}
				if n.Tok == token.DEFINE && id != nil && info.Defs[id] != nil {
					continue // new var declared by := has no effect
				}
				unknown() // assignment to existing var
				break
			}

		case *ast.GoStmt:
			visitExpr(n.Call.Fun)
			for _, arg := range n.Call.Args {
				visitExpr(arg)
			}
			unknown() // effect: create goroutine

		case *ast.DeferStmt:
			visitExpr(n.Call.Fun)
			for _, arg := range n.Call.Args {
				visitExpr(arg)
			}
			unknown() // effect: push defer

		case *ast.ReturnStmt:
			for _, res := range n.Results {
				visitExpr(res)
			}
			return false

		case *ast.BlockStmt:
			for _, stmt := range n.List {
				if !visitStmt(stmt) {
					return false
				}
			}

		case *ast.BranchStmt:
			unknown() // control flow

		case *ast.IfStmt:
			visitStmt(n.Init)
			visitExpr(n.Cond)
			unknown() // control flow

		case *ast.SwitchStmt:
			visitStmt(n.Init)
			visitExpr(n.Tag)
			unknown() // control flow

		case *ast.TypeSwitchStmt:
			visitStmt(n.Init)
			visitStmt(n.Assign)
			unknown() // control flow

		case *ast.SelectStmt:
			unknown() // control flow

		case *ast.ForStmt:
			visitStmt(n.Init)
			visitExpr(n.Cond)
			unknown() // control flow

		case *ast.RangeStmt:
			visitExpr(n.X)
			unknown() // control flow

		case *ast.EmptyStmt, *ast.BadStmt:
			// no effect

		case nil:
			// optional subtree

		default:
			panic(n)
		}
		return true
	}
	visitStmt(body)

	return effects
}
