// Copyright 2024 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.

// The deadlocals pass removes assignments to unused local variables.
package deadlocals

import (
	"cmd/compile/internal/base"
	"cmd/compile/internal/ir"
	"cmd/compile/internal/types"
	"cmd/internal/src"
	"fmt"
	"go/constant"
)

// Funcs applies the deadlocals pass to fns.
func Funcs(fns []*ir.Func) {
	if base.Flag.N != 0 || base.Debug.NoDeadLocals != 0 {
		return
	}

	zero := ir.NewBasicLit(base.AutogeneratedPos, types.Types[types.TINT], constant.MakeInt64(0))

	for _, fn := range fns {

		if fn.IsClosure() {
			if ir.MatchAstDump(fn, "deadlocals closure") {
				ir.AstDump(fn, "deadlocals closure skipped, "+ir.FuncName(fn))
			}
			continue
		}

		v := newVisitor(fn)
		v.nodes(fn.Body)

		for _, k := range v.defsKeys {
			assigns := v.defs[k]
			for _, as := range assigns {
				// Kludge for "missing func info" linker panic.
				// See also closureInitLSym in inline/inl.go.
				if clo, ok := (*as.rhs).(*ir.ClosureExpr); ok && clo.Op() == ir.OCLOSURE {
					if clo.Func.IsClosure() {
						ir.InitLSym(clo.Func, true)
					}
				}

				*as.lhs = ir.BlankNode
				*as.rhs = zero
			}
			if len(assigns) > 0 {
				// k.Defn might be pointing at one of the
				// assignments we're overwriting.
				k.Defn = nil
			}
		}
		if ir.MatchAstDump(fn, "deadlocals") {
			ir.AstDump(fn, "deadLocals, "+ir.FuncName(fn))
		}
	}
}

type visitor struct {
	curfn *ir.Func
	// defs[name] contains assignments that can be discarded if name can be discarded.
	// if defs[name] is defined nil, then name is actually used.
	defs     map[*ir.Name][]assign
	defsKeys []*ir.Name // insertion order of keys, for reproducible iteration (and builds)

	doNode func(ir.Node) bool
}

type assign struct {
	pos      src.XPos
	lhs, rhs *ir.Node
}

func newVisitor(fn *ir.Func) *visitor {
	v := &visitor{
		curfn: fn,
		defs:  make(map[*ir.Name][]assign),
	}
	v.doNode = func(n ir.Node) bool {
		v.node(n)
		return false
	}
	return v
}

func (v *visitor) node(n ir.Node) {
	if n == nil {
		return
	}

	switch n.Op() {
	default:
		ir.DoChildrenWithHidden(n, v.doNode)
	case ir.OCLOSURE:
		n := n.(*ir.ClosureExpr)
		v.nodes(n.Init())
		for _, cv := range n.Func.ClosureVars {
			v.node(cv)
		}
		v.nodes(n.Func.Body)

	case ir.ODCL:
		// ignore
	case ir.ONAME:
		n := n.(*ir.Name)
		n = n.Canonical()
		if isLocal(n, false) {
			// Force any lazy definitions.
			s, ok := v.defs[n]
			if !ok {
				v.defsKeys = append(v.defsKeys, n)
			}
			v.defs[n] = nil
			for _, as := range s {
				// do the visit that was skipped in v.assign when as was appended to v.defs[n]
				v.node(*as.rhs)
			}
		}

	case ir.OAS:
		n := n.(*ir.AssignStmt)
		v.assign(n.Pos(), &n.X, &n.Y, false)
	case ir.OAS2:
		n := n.(*ir.AssignListStmt)

		// If all LHS vars are blank, treat them as intentional
		// uses of corresponding RHS vars.  If any are non-blank
		// then any blanks are discards.
		hasNonBlank := false
		for i := range n.Lhs {
			if !ir.IsBlank(n.Lhs[i]) {
				hasNonBlank = true
				break
			}
		}
		for i := range n.Lhs {
			v.assign(n.Pos(), &n.Lhs[i], &n.Rhs[i], hasNonBlank)
		}
	}
}

func (v *visitor) nodes(list ir.Nodes) {
	for _, n := range list {
		v.node(n)
	}
}

func hasEffects(n ir.Node) bool {
	if n == nil {
		return false
	}
	if len(n.Init()) != 0 {
		return true
	}

	switch n.Op() {
	// TODO(mdempsky): More.
	case ir.ONAME, ir.OLITERAL, ir.ONIL, ir.OCLOSURE:
		return false
	}
	return true
}

func (v *visitor) assign(pos src.XPos, lhs, rhs *ir.Node, blankIsNotUse bool) {
	name, ok := (*lhs).(*ir.Name)
	if !ok {
		v.node(*lhs) // XXX: Interpret as variable, not value.
		v.node(*rhs)
		return
	}
	name = name.Canonical()

	if isLocal(name, blankIsNotUse) && !hasEffects(*rhs) {
		if s, ok := v.defs[name]; !ok || s != nil {
			// !ok || s != nil is FALSE if previously "v.defs[name] = nil" -- that marks a use.
			if !ok {
				v.defsKeys = append(v.defsKeys, name)
			}
			v.defs[name] = append(s, assign{pos, lhs, rhs})
			return // don't visit rhs unless that node ends up live, later.
		}
	}

	v.node(*rhs)
}

func isLocal(n *ir.Name, blankIsNotUse bool) bool {
	if ir.IsBlank(n) {
		// Treat single assignments as intentional use (false), anything else is a discard (true).
		return blankIsNotUse
	}

	switch n.Class {
	case ir.PAUTO, ir.PPARAM:
		return true
	case ir.PPARAMOUT:
		return false
	case ir.PEXTERN, ir.PFUNC:
		return false
	}
	panic(fmt.Sprintf("unexpected Class: %+v", n))
}
