// 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 inlheur

import (
	"cmd/compile/internal/ir"
	"fmt"
	"os"
)

// ShouldFoldIfNameConstant analyzes expression tree 'e' to see
// whether it contains only combinations of simple references to all
// of the names in 'names' with selected constants + operators. The
// intent is to identify expression that could be folded away to a
// constant if the value of 'n' were available. Return value is TRUE
// if 'e' does look foldable given the value of 'n', and given that
// 'e' actually makes reference to 'n'. Some examples where the type
// of "n" is int64, type of "s" is string, and type of "p" is *byte:
//
//	Simple?		Expr
//	yes			n<10
//	yes			n*n-100
//	yes			(n < 10 || n > 100) && (n >= 12 || n <= 99 || n != 101)
//	yes			s == "foo"
//	yes			p == nil
//	no			n<foo()
//	no			n<1 || n>m
//	no			float32(n)<1.0
//	no			*p == 1
//	no			1 + 100
//	no			1 / n
//	no			1 + unsafe.Sizeof(n)
//
// To avoid complexities (e.g. nan, inf) we stay way from folding and
// floating point or complex operations (integers, bools, and strings
// only). We also try to be conservative about avoiding any operation
// that might result in a panic at runtime, e.g. for "n" with type
// int64:
//
//	1<<(n-9) < 100/(n<<9999)
//
// we would return FALSE due to the negative shift count and/or
// potential divide by zero.
func ShouldFoldIfNameConstant(n ir.Node, names []*ir.Name) bool {
	cl := makeExprClassifier(names)
	var doNode func(ir.Node) bool
	doNode = func(n ir.Node) bool {
		ir.DoChildren(n, doNode)
		cl.Visit(n)
		return false
	}
	doNode(n)
	if cl.getdisp(n) != exprSimple {
		return false
	}
	for _, v := range cl.names {
		if !v {
			return false
		}
	}
	return true
}

// exprClassifier holds intermediate state about nodes within an
// expression tree being analyzed by ShouldFoldIfNameConstant. Here
// "name" is the name node passed in, and "disposition" stores the
// result of classifying a given IR node.
type exprClassifier struct {
	names       map[*ir.Name]bool
	disposition map[ir.Node]disp
}

type disp int

const (
	// no info on this expr
	exprNoInfo disp = iota

	// expr contains only literals
	exprLiterals

	// expr is legal combination of literals and specified names
	exprSimple
)

func (d disp) String() string {
	switch d {
	case exprNoInfo:
		return "noinfo"
	case exprSimple:
		return "simple"
	case exprLiterals:
		return "literals"
	default:
		return fmt.Sprintf("unknown<%d>", d)
	}
}

func makeExprClassifier(names []*ir.Name) *exprClassifier {
	m := make(map[*ir.Name]bool, len(names))
	for _, n := range names {
		m[n] = false
	}
	return &exprClassifier{
		names:       m,
		disposition: make(map[ir.Node]disp),
	}
}

// Visit sets the classification for 'n' based on the previously
// calculated classifications for n's children, as part of a bottom-up
// walk over an expression tree.
func (ec *exprClassifier) Visit(n ir.Node) {

	ndisp := exprNoInfo

	binparts := func(n ir.Node) (ir.Node, ir.Node) {
		if lex, ok := n.(*ir.LogicalExpr); ok {
			return lex.X, lex.Y
		} else if bex, ok := n.(*ir.BinaryExpr); ok {
			return bex.X, bex.Y
		} else {
			panic("bad")
		}
	}

	t := n.Type()
	if t == nil {
		if debugTrace&debugTraceExprClassify != 0 {
			fmt.Fprintf(os.Stderr, "=-= *** untyped op=%s\n",
				n.Op().String())
		}
	} else if t.IsInteger() || t.IsString() || t.IsBoolean() || t.HasNil() {
		switch n.Op() {
		// FIXME: maybe add support for OADDSTR?
		case ir.ONIL:
			ndisp = exprLiterals

		case ir.OLITERAL:
			if _, ok := n.(*ir.BasicLit); ok {
			} else {
				panic("unexpected")
			}
			ndisp = exprLiterals

		case ir.ONAME:
			nn := n.(*ir.Name)
			if _, ok := ec.names[nn]; ok {
				ndisp = exprSimple
				ec.names[nn] = true
			} else {
				sv := ir.StaticValue(n)
				if sv.Op() == ir.ONAME {
					nn = sv.(*ir.Name)
				}
				if _, ok := ec.names[nn]; ok {
					ndisp = exprSimple
					ec.names[nn] = true
				}
			}

		case ir.ONOT,
			ir.OPLUS,
			ir.ONEG:
			uex := n.(*ir.UnaryExpr)
			ndisp = ec.getdisp(uex.X)

		case ir.OEQ,
			ir.ONE,
			ir.OLT,
			ir.OGT,
			ir.OGE,
			ir.OLE:
			// compare ops
			x, y := binparts(n)
			ndisp = ec.dispmeet(x, y)
			if debugTrace&debugTraceExprClassify != 0 {
				fmt.Fprintf(os.Stderr, "=-= meet(%s,%s) = %s for op=%s\n",
					ec.getdisp(x), ec.getdisp(y), ec.dispmeet(x, y),
					n.Op().String())
			}
		case ir.OLSH,
			ir.ORSH,
			ir.ODIV,
			ir.OMOD:
			x, y := binparts(n)
			if ec.getdisp(y) == exprLiterals {
				ndisp = ec.dispmeet(x, y)
			}

		case ir.OADD,
			ir.OSUB,
			ir.OOR,
			ir.OXOR,
			ir.OMUL,
			ir.OAND,
			ir.OANDNOT,
			ir.OANDAND,
			ir.OOROR:
			x, y := binparts(n)
			if debugTrace&debugTraceExprClassify != 0 {
				fmt.Fprintf(os.Stderr, "=-= meet(%s,%s) = %s for op=%s\n",
					ec.getdisp(x), ec.getdisp(y), ec.dispmeet(x, y),
					n.Op().String())
			}
			ndisp = ec.dispmeet(x, y)
		}
	}

	if debugTrace&debugTraceExprClassify != 0 {
		fmt.Fprintf(os.Stderr, "=-= op=%s disp=%v\n", n.Op().String(),
			ndisp.String())
	}

	ec.disposition[n] = ndisp
}

func (ec *exprClassifier) getdisp(x ir.Node) disp {
	if d, ok := ec.disposition[x]; ok {
		return d
	} else {
		panic("missing node from disp table")
	}
}

// dispmeet performs a "meet" operation on the data flow states of
// node x and y (where the term "meet" is being drawn from traditional
// lattice-theoretical data flow analysis terminology).
func (ec *exprClassifier) dispmeet(x, y ir.Node) disp {
	xd := ec.getdisp(x)
	if xd == exprNoInfo {
		return exprNoInfo
	}
	yd := ec.getdisp(y)
	if yd == exprNoInfo {
		return exprNoInfo
	}
	if xd == exprSimple || yd == exprSimple {
		return exprSimple
	}
	if xd != exprLiterals || yd != exprLiterals {
		panic("unexpected")
	}
	return exprLiterals
}
