package nilness

import (
	"fmt"
	"go/constant"
	"go/token"
	"go/types"
	"reflect"
	"slices"
	"strings"

	"honnef.co/go/tools/analysis/dfa"
	"honnef.co/go/tools/analysis/dfa/dense"
	"honnef.co/go/tools/go/ir"
	"honnef.co/go/tools/go/types/typeutil"
	"honnef.co/go/tools/internal/passes/buildir"

	"golang.org/x/exp/typeparams"
	"golang.org/x/tools/go/analysis"
)

// TODO(dh): The analysis is currently entirely forward, which means that for
//
// 	x := s[:0]
// 	y := s[:1]
// 	z := s[:0]
//
// x will have MaybeNil at every program point and s will have MaybeNil before
// execution of y, even though executing y without panicing tells us that s has
// been non-nil for all 3 instructions.

type nilnessFact struct {
	Rets []ValueNilness
}

func (*nilnessFact) AFact() {}
func (fact *nilnessFact) String() string {
	return fmt.Sprintf("nilness: %v", fact.Rets)
}

type ValueNilness struct {
	// Undefined for non-interface values.
	// For interface values, whether the stored value may be nil.
	// Even when Outer == MaybeNil, Inner may still offer precise information
	// for the cases when Outer is dynamically not nil. For example, {NeverNil,
	// MaybeNil} states that the interface value might be nil, but if it isn't,
	// it will definitely contain a non-nil value.
	Inner Nilness
	// For non-interface values, whether the value may be nil.
	// For interface values, whether the interface value may be nil.
	Outer Nilness
}

type Result struct {
	m map[*types.Func][]ValueNilness
}

var Analysis = &analysis.Analyzer{
	Name:       "nilness",
	Doc:        "Annotates return values with their nilness",
	Run:        run,
	Requires:   []*analysis.Analyzer{buildir.Analyzer},
	FactTypes:  []analysis.Fact{(*nilnessFact)(nil)},
	ResultType: reflect.TypeFor[*Result](),
}

// Nilness returns nilness information for return value ret of fn.
func (r *Result) Nilness(fn *types.Func, ret int) ValueNilness {
	typ := fn.Type().(*types.Signature).Results().At(ret).Type()
	if !typeutil.IsPointerLike(typ) {
		return ValueNilness{Outer: NeverNil}
	}
	if len(r.m[fn]) == 0 {
		return ValueNilness{Inner: MaybeNil, Outer: MaybeNil}
	}

	return normalize(r.m[fn][ret], typ)
}

func normalize(v ValueNilness, typ types.Type) ValueNilness {
	if v.Inner == 0 || !types.IsInterface(typ) {
		v.Inner = MaybeNil
	}
	if v.Outer == 0 {
		v.Outer = MaybeNil
	}
	return v
}

func run(pass *analysis.Pass) (any, error) {
	seen := map[*ir.Function]struct{}{}
	out := &Result{
		m: map[*types.Func][]ValueNilness{},
	}

	// TODO(dh): instead of recursion and giving up on mutual recursion, we
	// should compute the DFA over the call graph, at least until we have
	// proper function summaries.
	for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
		impl(pass, fn, seen)
	}

	for _, fact := range pass.AllObjectFacts() {
		out.m[fact.Object.(*types.Func)] = fact.Fact.(*nilnessFact).Rets
	}

	return out, nil
}

type Nilness uint8

const (
	// The value is never nil.
	NeverNil Nilness = iota + 1
	// The value is always nil.
	AlwaysNil
	// The value might be nil, but only because of the value of a global
	// variable.
	MaybeNilGlobal
	// The value might be nil.
	MaybeNil
)

func (n Nilness) String() string {
	switch n {
	case 0:
		return "NoNilness"
	case NeverNil:
		return "NeverNil"
	case AlwaysNil:
		return "AlwaysNil"
	case MaybeNilGlobal:
		return "MaybeNilGlobal"
	case MaybeNil:
		return "MaybeNil"
	default:
		return "InvalidNilness"
	}
}

type state struct {
	cloned bool
	m      []ValueNilness
	n      numbering
}

func (s *state) get(v ir.Value) ValueNilness {
	if !typeutil.IsPointerLike(v.Type()) {
		// All non-pointer-like types are always {_ NeverNil}.
		return ValueNilness{Outer: NeverNil}
	}
	num := s.n.number(v)
	if num < len(s.m) {
		return s.m[num]
	}

	switch v.(type) {
	case *ir.Parameter:
		return ValueNilness{Inner: MaybeNil, Outer: MaybeNil}
	case *ir.Builtin:
		return ValueNilness{Outer: NeverNil}
	case *ir.FreeVar:
		return ValueNilness{Inner: MaybeNil, Outer: MaybeNil}
	case *ir.Function:
		return ValueNilness{Outer: NeverNil}
	case *ir.Global:
		// Globals are addresses, not the values stored in them. The addresses
		// cannot be nil.
		return ValueNilness{Outer: NeverNil}
	}

	return lattice{}.Ident()
}

func (s *state) set(key ir.Value, value ValueNilness) {
	if !typeutil.IsPointerLike(key.Type()) {
		// No point in recording state for non-pointer-like types. They're
		// always {_ NeverNil}.
		return
	}

	if value == (lattice{}.Ident()) {
		// No point in storing the default value.
		return
	}
	num := s.n.number(key)
	if !s.cloned {
		if num < len(s.m) && s.m[num] == value {
			// Don't clone if the value already matches.
			return
		}
		s.cloned = true
		s.m = slices.Clone(s.m)
	}
	if num >= len(s.m) {
		s.m = append(s.m, make([]ValueNilness, num-len(s.m)+1)...)
	}
	s.m[num] = value
}

func (s *state) setInner(key ir.Value, value Nilness) {
	if !typeutil.IsPointerLike(key.Type()) {
		return
	}
	if value == (lattice{}.Ident().Inner) {
		return
	}
	num := s.n.number(key)
	if !s.cloned {
		if num < len(s.m) && s.m[num].Inner == value {
			// Don't clone if the value already matches.
			return
		}
		s.cloned = true
		s.m = slices.Clone(s.m)
	}
	if num >= len(s.m) {
		dflt := s.get(key)
		s.m = append(s.m, make([]ValueNilness, num-len(s.m)+1)...)
		s.m[num] = dflt
	}
	v := s.m[num]
	v.Inner = value
	s.m[num] = v
}

func (s *state) setOuter(key ir.Value, value Nilness) {
	if !typeutil.IsPointerLike(key.Type()) {
		return
	}
	if value == (lattice{}).Ident().Outer {
		return
	}
	num := s.n.number(key)
	if !s.cloned {
		if num < len(s.m) && s.m[num].Outer == value {
			// Don't clone if the value already matches.
			return
		}
		s.cloned = true
		s.m = slices.Clone(s.m)
	}
	if num >= len(s.m) {
		dflt := s.get(key)
		s.m = append(s.m, make([]ValueNilness, num-len(s.m)+1)...)
		s.m[num] = dflt
	}
	v := s.m[num]
	v.Outer = value
	s.m[num] = v
}

func defaultNilnessForSignature(pass *analysis.Pass, typ *types.Signature) []ValueNilness {
	n := typ.Results().Len()
	if n == 0 {
		return nil
	}
	out := make([]ValueNilness, n)
	for i := range n {
		out[i] = defaultNilness(pass, typ.Results().At(i).Type())
	}
	return out
}

func defaultNilness(pass *analysis.Pass, typ types.Type) ValueNilness {
	if typeutil.IsPointerLike(typ) {
		// IsPointerLike handles type parameters with type sets, too.
		return ValueNilness{MaybeNil, MaybeNil}
	} else {
		return ValueNilness{NeverNil, NeverNil}
	}
}

func impl(pass *analysis.Pass, fn *ir.Function, seenFns map[*ir.Function]struct{}) []ValueNilness {
	goto start
bailout:
	return defaultNilnessForSignature(pass, fn.Signature)

start:
	if fn.Signature.Results().Len() == 0 {
		return nil
	}
	if fn.Object() == nil {
		// TODO(dh): support closures
		goto bailout
	}
	if fact := new(nilnessFact); pass.ImportObjectFact(fn.Object(), fact) {
		return fact.Rets
	}
	if fn.Pkg != pass.ResultOf[buildir.Analyzer].(*buildir.IR).Pkg {
		goto bailout
	}
	if fn.Blocks == nil {
		goto bailout
	}
	if _, ok := seenFns[fn]; ok {
		// break recursion
		goto bailout
	}

	seenFns[fn] = struct{}{}

	anyPointers := false
	for ret := range fn.Signature.Results().Variables() {
		if typeutil.IsPointerLike(ret.Type()) {
			anyPointers = true
			break
		}
	}

	if !anyPointers {
		goto bailout
	}

	n := numbering{}

	processBlock := func(from, to *ir.BasicBlock, s state) state {
		handleReturnValue := func(v ir.Value, call *ir.Call, idx int) {
			typ := call.Common().Signature().Results().At(idx).Type()
			if !typeutil.IsPointerLike(typ) {
				s.setOuter(v, NeverNil)
				return
			}

			if callee, ok := call.Call.Value.(*ir.Builtin); ok {
				switch callee.Name() {
				case "append":
					// TODO(dh): if we knew that the varargs had non-zero
					// length, we'd know that the resulting slice is non-nil.
					switch an := s.get(call.Call.Args[0]).Outer; an {
					case MaybeNil, MaybeNilGlobal, NeverNil:
						s.setOuter(v, an)
					case AlwaysNil:
						s.setOuter(v, MaybeNil)
					}
				case "UnsafeSlice":
					// If len is negative, or if ptr is nil and len is not
					// zero, unsafe.Slice panics. This implies that a non-nil
					// pointer cannot become nil, and vice versa.
					s.set(v, s.get(call.Call.Args[0]))
				case "UnsafeStringData":
					// TODO(dh): if we had string length information we could
					// return better information.
					s.setOuter(v, MaybeNil)
				case "UnsafeSliceData":
					// When the slice is non-nil but has zero capacity, the
					// returned pointer is still non-nil, so we don't have to
					// worry about that.
					s.set(v, s.get(call.Call.Args[0]))
				case "UnsafeAdd":
					// TODO(dh): a positive addend can never result in a nil pointer.

					// Pointer arithmetic can turn nil pointers into non-nil
					// ones and vice versa.
					s.setOuter(v, MaybeNil)
				case "ssa:deferstack":
					s.setOuter(v, NeverNil)
				case "ssa:wrapnilchk":
					s.setOuter(v, NeverNil)
				default:
					panic(fmt.Sprintf("internal error: unhandled builtin %s", callee.Name()))
				}
				return
			}

			callee := call.Common().StaticCallee()
			if callee == nil {
				// We don't know which function is being called.
				s.set(v, ValueNilness{MaybeNil, MaybeNil})
				return
			}
			calleeNilness := impl(pass, callee, seenFns)
			if len(calleeNilness) > idx {
				s.set(v, normalize(calleeNilness[idx], typ))
			} else {
				s.set(v, ValueNilness{MaybeNil, MaybeNil})
			}
		}

		for _, instr := range from.Instrs {
			// It is tempting to return early when instr is an ir.Value that
			// doesn't have pointer type. However, instructions like ir.Load
			// tell us something about the value being operated on.

			switch v := instr.(type) {
			case *ir.Convert:
				s.set(v, s.get(v.X))
			case *ir.SliceToArrayPointer:
				// Go does not currently allow (*T)(s) where T is a type
				// parameter with a type set consisting of array types, but it
				// does allow (T)(s) where T is a type parameter with a type
				// set consisting of pointers to array types.

				allNonZero := typeutil.All(v.Type(), func(term *types.Term) bool {
					ptr := term.Type().Underlying().(*types.Pointer).Elem()
					return typeutil.All(ptr, func(innerTerm *types.Term) bool {
						return innerTerm.Type().Underlying().(*types.Array).Len() != 0
					})
				})

				if allNonZero {
					// converting a slice to an array pointer of length > 0
					// panics if the slice is nil
					s.setOuter(v, NeverNil)
					s.setOuter(v.X, NeverNil)
				} else {
					s.set(v, s.get(v.X))
				}
			case *ir.SliceToArray:
				// Pretty much the same logic as SliceToArrayPointer, minus the
				// pointer.

				allNonZero := typeutil.All(v.Type(), func(term *types.Term) bool {
					return term.Type().Underlying().(*types.Array).Len() != 0
				})

				if allNonZero {
					// converting a slice to an array of length > 0
					// panics if the slice is nil
					s.setOuter(v.X, NeverNil)
				}
			case *ir.Slice:
				if typeutil.All(v.X.Type(), typeutil.IsType[*types.Array]) {
					// Slicing arrays never results in a nil slice.
					s.setOuter(v, NeverNil)
					continue
				}

				// checkBound returns true if one of the bounds (low, high,
				// capacity) has non-zero value.
				checkBound := func(v ir.Value) bool {
					if v == nil {
						return false
					}
					// TODO(dh): this is where integration with constant
					// propagation and value range analysis would be useful.
					if k, ok := v.(*ir.Const); ok {
						kv, ok := constant.Int64Val(k.Value)
						return !ok || kv != 0
					}
					return false
				}
				if checkBound(v.Low) || checkBound(v.High) || checkBound(v.Max) {
					// One of the indices is non-zero, which means slicing can
					// only succeed if the slicee is not nil.
					s.setOuter(v, NeverNil)
					s.setOuter(v.X, NeverNil)
				} else {
					// The new slice is as nilly as the slicee.
					s.set(v, s.get(v.X))
				}

			case *ir.If:
				cond := v.Cond
				binop, ok := cond.(*ir.BinOp)
				if !ok {
					continue
				}
				isNil := func(v ir.Value) bool {
					k, ok := v.(*ir.Const)
					if !ok {
						return false
					}
					return k.Value == nil
				}
				var target ir.Value
				if isNil(binop.X) {
					target = binop.Y
				} else if isNil(binop.Y) {
					target = binop.X
				} else {
					continue
				}
				op := binop.Op
				if to != from.Succs[0] {
					// we're in the false branch, negate op
					switch op {
					case token.EQL:
						op = token.NEQ
					case token.NEQ:
						op = token.EQL
					default:
						panic(fmt.Sprintf("internal error: unhandled token %v", op))
					}
				}
				switch op {
				case token.EQL:
					s.set(target, ValueNilness{AlwaysNil, AlwaysNil})
				case token.NEQ:
					s.setOuter(target, NeverNil)
				default:
					panic(fmt.Sprintf("internal error: unhandled token %v", op))
				}

				// TODO(dh): also handle comparison of two non-nil values. The
				// true branch of neverNil == nilly makes the nilly value neverNil.
			case *ir.ChangeType:
				s.set(v, s.get(v.X))
			case *ir.MultiConvert:
				s.set(v, s.get(v.X))
			case *ir.Load:
				if _, ok := v.X.(*ir.Global); ok {
					s.setOuter(v, MaybeNilGlobal)
				} else {
					s.setOuter(v, MaybeNil)
				}
				s.setOuter(v.X, NeverNil)
			case *ir.FieldAddr:
				s.setOuter(v.X, NeverNil)
				s.setOuter(v, NeverNil)
			case *ir.IndexAddr:
				s.setOuter(v.X, NeverNil)
				s.setOuter(v, NeverNil)
			case *ir.Alloc, *ir.MakeMap, *ir.MakeSlice, *ir.MakeClosure, *ir.MakeChan:
				s.setOuter(v.(ir.Value), NeverNil)
			case *ir.MapUpdate:
				s.setOuter(v.Map, NeverNil)
			case *ir.Store:
				s.setOuter(v.Addr, NeverNil)
			case ir.CallInstruction:
				// go/defer/calling a nil function fatals/panics
				if !v.Common().IsInvoke() {
					s.setOuter(v.Common().Value, NeverNil)
				}
				_, ok := v.(*ir.Call)
				if !ok {
					// Defer and Go don't produce values
					continue
				}
				if v.Common().Signature().Results().Len() != 1 {
					// If the called function doesn't return any values then we
					// don't care about it. If it has more than one return
					// value, they'll be handled by Extract.
					continue
				}

				handleReturnValue(v.(ir.Value), v.(*ir.Call), 0)
			case *ir.Send:
				s.setOuter(v.Chan, NeverNil)
			case *ir.Recv:
				s.setOuter(v.Chan, NeverNil)
			case *ir.MakeInterface:
				s.set(v, ValueNilness{
					Inner: s.get(v.X).Outer,
					Outer: NeverNil,
				})
			case *ir.ChangeInterface:
				s.set(v, s.get(v.X))
			case *ir.TypeAssert:
				if !v.CommaOk {
					// The interface value cannot have been nil, or the type
					// assertion would have panicked.
					s.setOuter(v.X, NeverNil)

					if types.IsInterface(v.Type()) && !typeparams.IsTypeParam(v.Type()) {
						// Type asserting to another interface doesn't succeed
						// if the assertee was nil. It also results in a new
						// interface value.
						s.setOuter(v, NeverNil)
						s.setInner(v, s.get(v.X).Inner)
					} else {
						// We've extracted the interface value's inner value.
						s.setOuter(v, s.get(v.X).Inner)
					}
				} else {
					// In a comma-ok type assertion, the return type is a
					// tuple. There'll be Extract instructions getting the
					// individual values, to which we'll attach the nilness
					// info.
				}
			case *ir.TypeSwitch:
				// Handled in Extract
			case *ir.MapLookup:
				if s.get(v.X).Outer == AlwaysNil {
					s.set(v, ValueNilness{AlwaysNil, AlwaysNil})
				} else {
					s.set(v, ValueNilness{MaybeNil, MaybeNil})
				}
			case *ir.Field:
				s.set(v.X, ValueNilness{NeverNil, NeverNil})
				s.set(v, ValueNilness{MaybeNil, MaybeNil})
			case *ir.Index:
				s.set(v.X, ValueNilness{NeverNil, NeverNil})
				s.set(v, ValueNilness{MaybeNil, MaybeNil})

			case *ir.Extract:
				switch tuple := v.Tuple.(type) {
				case *ir.TypeAssert:
					// When we get here, the type assertion used the comma-ok
					// form, and we don't yet know anything about the result of
					// the type assertion.
					if v.Index == 0 {
						s.set(v, ValueNilness{MaybeNil, MaybeNil})
					}

					// TODO(dh): We should set v's nilness in the true and
					// false branches of checks on the ok value. However, ok can be
					// used in arbitrary ways, and we're also not set up to handle
					// relational facts (ok being true or false affects the value
					// of the other Extract).

				case *ir.Call:
					handleReturnValue(v, tuple, v.Index)

				case *ir.TypeSwitch:
					if v.Index == 0 {
						// Index 0 is an integer and not interesting.
						continue
					}
					idx := v.Index - 1
					if idx >= len(tuple.Conds) {
						// Default branch

						// If there is an untyped nil case, then being in the
						// default branch tells us that the interface value
						// isn't nil.
						hasNil := slices.ContainsFunc(tuple.Conds, func(typ types.Type) bool {
							if typ, ok := typ.(*types.Basic); ok && typ.Kind() == types.UntypedNil {
								return true
							}
							return false
						})
						if hasNil {
							s.setOuter(tuple.Tag, NeverNil)
						} else {
							s.setOuter(tuple.Tag, MaybeNil)
						}
						s.setOuter(v, s.get(tuple.Tag).Inner)
					} else {
						// There is no Extract for the 'untyped nil' case,
						// which means that executing any Extract from a type
						// switch implies that the switched-over value wasn't a
						// nil interface value.
						s.setOuter(tuple.Tag, NeverNil)
						typ := tuple.Conds[idx]
						if types.IsInterface(typ) && !typeparams.IsTypeParam(typ) {
							// Succesfully type asserting to an interface type
							// always produces a non-nil interface value.
							s.setInner(v, s.get(tuple.Tag).Inner)
							s.setOuter(v, NeverNil)
						} else {
							s.setOuter(v, s.get(tuple.Tag).Inner)
						}
					}
				default:
					s.set(v, ValueNilness{MaybeNil, MaybeNil})
				}
			case *ir.Select:
				if v.Blocking && len(v.States) == 1 {
					// If the select doesn't have a default branch and only has
					// one state, that state's channel cannot have been nil if
					// we finished execution the select.
					s.setOuter(v.States[0].Chan, NeverNil)
				}
			case *ir.DebugRef, *ir.Jump, *ir.BlankStore, *ir.Phi,
				*ir.Panic, *ir.Return, *ir.RunDefers, *ir.Unreachable, *ir.ConstantSwitch,
				*ir.UnOp, *ir.BinOp, *ir.CompositeValue, *ir.Range, *ir.Next:
			default:
				posn := pass.Fset.PositionFor(v.Pos(), false)
				panic(fmt.Sprintf("internal error: unhandled type %T at %s", v, posn))
			}
		}
		return s
	}

	processPhis := func(b *ir.BasicBlock, i int, s state) state {
		for _, instr := range b.Instrs {
			if instr, ok := instr.(*ir.Phi); ok {
				s.set(instr, s.get(instr.Edges[i]))
			} else {
				break
			}
		}
		return s
	}

	// Populate default state for non-instruction values we encounter. We
	// cannot defer this logic to state.get because control flow merges use
	// simple merges of lattice values and won't know about value-specific
	// defaults.
	entrys := state{cloned: true, n: n}
	for _, param := range fn.Params {
		if typeutil.IsPointerLike(param.Type()) {
			entrys.set(param, ValueNilness{Inner: MaybeNil, Outer: MaybeNil})
		} else {
			// We never track nilness for value types, so they don't have to be
			// present in the entry state, either.
		}
	}
	if strings.HasPrefix(fn.Synthetic, "bound method wrapper") {
		// This is a bound method and the bound receiver might be nil
		for _, fvar := range fn.FreeVars {
			entrys.set(fvar, ValueNilness{Outer: MaybeNil})
		}
	} else {
		// This is a closure, and closed over variables are allocs, which
		// cannot be nil.
		for _, fvar := range fn.FreeVars {
			entrys.set(fvar, ValueNilness{Outer: NeverNil})
		}
	}
	var ops []*ir.Value
	for _, b := range fn.Blocks {
		for _, instr := range b.Instrs {
			ops = instr.Operands(ops[:0])
			for _, pop := range ops {
				if op, ok := (*pop).(*ir.Const); ok && typeutil.IsPointerLike(op.Type()) {
					// The only constant pointer-like is nil.
					entrys.set(op, ValueNilness{Inner: AlwaysNil, Outer: AlwaysNil})
				}
			}
		}
	}
	res := dense.Forward[dfa.DenseMapLattice[ValueNilness, lattice]](
		fn,
		map[int][]ValueNilness{0: entrys.m},
		func(fromID, toID int, in []ValueNilness) []ValueNilness {
			from := fn.Blocks[fromID]
			to := fn.Blocks[toID]
			s := state{n: n, m: in}
			s = processBlock(from, to, s)
			i := slices.Index(to.Preds, from)
			s = processPhis(to, i, s)
			return s.m
		},
	)

	retNilness := make([]ValueNilness, fn.Signature.Results().Len())
	for b := range fn.Returns() {
		ret := b.Control().(*ir.Return)
		s := state{n: n, m: res.In(b.Index)}
		s = processBlock(b, nil, s)
		for i, res := range ret.Results {
			retNilness[i] = lattice{}.Merge(retNilness[i], s.get(res))
		}
	}

	interesting := false
	for i := range retNilness {
		typ := fn.Signature.Results().At(i).Type()
		if !typeutil.IsPointerLike(typ) {
			retNilness[i] = ValueNilness{NeverNil, NeverNil}
			continue
		}
		retNilness[i] = normalize(retNilness[i], typ)
		if retNilness[i] != (ValueNilness{MaybeNil, MaybeNil}) {
			interesting = true
		}
	}

	if interesting {
		pass.ExportObjectFact(fn.Object(), &nilnessFact{retNilness})
	}

	return retNilness
}

type lattice struct{}

var _ dfa.Semilattice[ValueNilness] = lattice{}

// Equals implements [dfa.Semilattice].
func (l lattice) Equals(a, b ValueNilness) bool {
	return a == b
}

// Ident implements [dfa.Semilattice].
func (l lattice) Ident() ValueNilness {
	return ValueNilness{}
}

var latticeMerge = [5][5]Nilness{
	0: {
		0:              0,
		NeverNil:       NeverNil,
		AlwaysNil:      AlwaysNil,
		MaybeNilGlobal: MaybeNilGlobal,
		MaybeNil:       MaybeNil,
	},
	NeverNil: {
		0:              NeverNil,
		NeverNil:       NeverNil,
		AlwaysNil:      MaybeNil,
		MaybeNilGlobal: MaybeNilGlobal,
		MaybeNil:       MaybeNil,
	},
	AlwaysNil: {
		0:              AlwaysNil,
		NeverNil:       MaybeNil,
		AlwaysNil:      AlwaysNil,
		MaybeNilGlobal: MaybeNil,
		MaybeNil:       MaybeNil,
	},
	MaybeNilGlobal: {
		0:              MaybeNilGlobal,
		NeverNil:       MaybeNilGlobal,
		AlwaysNil:      MaybeNil,
		MaybeNilGlobal: MaybeNilGlobal,
		MaybeNil:       MaybeNil,
	},
	MaybeNil: {
		0:              MaybeNil,
		NeverNil:       MaybeNil,
		AlwaysNil:      MaybeNil,
		MaybeNilGlobal: MaybeNil,
		MaybeNil:       MaybeNil,
	},
}

// Merge implements [dfa.Semilattice].
func (l lattice) Merge(a, b ValueNilness) ValueNilness {
	return ValueNilness{
		Inner: latticeMerge[a.Inner][b.Inner],
		Outer: latticeMerge[a.Outer][b.Outer],
	}
}

type numbering map[ir.Value]int

func (n numbering) number(v ir.Value) int {
	i, ok := n[v]
	if !ok {
		i = len(n)
		n[v] = i
	}
	return i
}
