// Copyright 2021 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 typecheck

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

// crawlExports crawls the type/object graph rooted at the given list of exported
// objects (which are variables, functions, and types). It descends through all parts
// of types and follows methods on defined types. Any functions that are found to be
// potentially callable by importers directly or after inlining are marked with
// ExportInline, so that iexport.go knows to export their inline body.
//
// The overall purpose of crawlExports is to AVOID exporting inlineable methods
// that cannot actually be referenced, thereby reducing the size of the exports
// significantly.
//
// For non-generic defined types reachable from global variables, we only set
// ExportInline for exported methods. For defined types that are directly named or are
// embedded recursively in such a type, we set ExportInline for all methods, since
// these types can be embedded in another local type. For instantiated types that are
// used anywhere in a inlineable function, we set ExportInline on all methods of the
// base generic type, since all methods will be needed for creating any instantiated
// type.
func crawlExports(exports []*ir.Name) {
	p := crawler{
		marked:         make(map[*types.Type]bool),
		embedded:       make(map[*types.Type]bool),
		generic:        make(map[*types.Type]bool),
		checkFullyInst: make(map[*types.Type]bool),
	}
	for _, n := range exports {
		p.markObject(n)
	}
}

type crawler struct {
	marked         map[*types.Type]bool // types already seen by markType
	embedded       map[*types.Type]bool // types already seen by markEmbed
	generic        map[*types.Type]bool // types already seen by markGeneric
	checkFullyInst map[*types.Type]bool // types already seen by checkForFullyInst
}

// markObject visits a reachable object (function, method, global type, or global variable)
func (p *crawler) markObject(n *ir.Name) {
	if n.Op() == ir.ONAME && n.Class == ir.PFUNC {
		p.markInlBody(n)
	}

	// If a declared type name is reachable, users can embed it in their
	// own types, which makes even its unexported methods reachable.
	if n.Op() == ir.OTYPE {
		p.markEmbed(n.Type())
	}

	p.markType(n.Type())
}

// markType recursively visits types reachable from t to identify functions whose
// inline bodies may be needed. For instantiated generic types, it visits the base
// generic type, which has the relevant methods.
func (p *crawler) markType(t *types.Type) {
	if orig := t.OrigType(); orig != nil {
		// Convert to the base generic type.
		t = orig
	}
	if p.marked[t] {
		return
	}
	p.marked[t] = true

	// If this is a defined type, mark all of its associated
	// methods. Skip interface types because t.Methods contains
	// only their unexpanded method set (i.e., exclusive of
	// interface embeddings), and the switch statement below
	// handles their full method set.
	if t.Sym() != nil && t.Kind() != types.TINTER {
		for _, m := range t.Methods().Slice() {
			if types.IsExported(m.Sym.Name) {
				p.markObject(m.Nname.(*ir.Name))
			}
		}
	}

	// Recursively mark any types that can be produced given a
	// value of type t: dereferencing a pointer; indexing or
	// iterating over an array, slice, or map; receiving from a
	// channel; accessing a struct field or interface method; or
	// calling a function.
	//
	// Notably, we don't mark function parameter types, because
	// the user already needs some way to construct values of
	// those types.
	switch t.Kind() {
	case types.TPTR, types.TARRAY, types.TSLICE:
		p.markType(t.Elem())

	case types.TCHAN:
		if t.ChanDir().CanRecv() {
			p.markType(t.Elem())
		}

	case types.TMAP:
		p.markType(t.Key())
		p.markType(t.Elem())

	case types.TSTRUCT:
		if t.IsFuncArgStruct() {
			break
		}
		for _, f := range t.FieldSlice() {
			// Mark the type of a unexported field if it is a
			// fully-instantiated type, since we create and instantiate
			// the methods of any fully-instantiated type that we see
			// during import (see end of typecheck.substInstType).
			if types.IsExported(f.Sym.Name) || f.Embedded != 0 ||
				isPtrFullyInstantiated(f.Type) {
				p.markType(f.Type)
			}
		}

	case types.TFUNC:
		for _, f := range t.Results().FieldSlice() {
			p.markType(f.Type)
		}

	case types.TINTER:
		for _, f := range t.AllMethods().Slice() {
			if types.IsExported(f.Sym.Name) {
				p.markType(f.Type)
			}
		}

	case types.TTYPEPARAM:
		// No other type that needs to be followed.
	}
}

// markEmbed is similar to markType, but handles finding methods that
// need to be re-exported because t can be embedded in user code
// (possibly transitively).
func (p *crawler) markEmbed(t *types.Type) {
	if t.IsPtr() {
		// Defined pointer type; not allowed to embed anyway.
		if t.Sym() != nil {
			return
		}
		t = t.Elem()
	}

	if orig := t.OrigType(); orig != nil {
		// Convert to the base generic type.
		t = orig
	}

	if p.embedded[t] {
		return
	}
	p.embedded[t] = true

	// If t is a defined type, then re-export all of its methods. Unlike
	// in markType, we include even unexported methods here, because we
	// still need to generate wrappers for them, even if the user can't
	// refer to them directly.
	if t.Sym() != nil && t.Kind() != types.TINTER {
		for _, m := range t.Methods().Slice() {
			p.markObject(m.Nname.(*ir.Name))
		}
	}

	// If t is a struct, recursively visit its embedded fields.
	if t.IsStruct() {
		for _, f := range t.FieldSlice() {
			if f.Embedded != 0 {
				p.markEmbed(f.Type)
			}
		}
	}
}

// markGeneric takes an instantiated type or a base generic type t, and marks all the
// methods of the base generic type of t. If a base generic type is written out for
// export, even if not explicitly marked for export, then all of its methods need to
// be available for instantiation, since we always create all methods of a specified
// instantiated type. Non-exported methods must generally be instantiated, since they may
// be called by the exported methods or other generic function in the same package.
func (p *crawler) markGeneric(t *types.Type) {
	if t.IsPtr() {
		t = t.Elem()
	}
	if orig := t.OrigType(); orig != nil {
		// Convert to the base generic type.
		t = orig
	}
	if p.generic[t] {
		return
	}
	p.generic[t] = true

	if t.Sym() != nil && t.Kind() != types.TINTER {
		for _, m := range t.Methods().Slice() {
			p.markObject(m.Nname.(*ir.Name))
		}
	}
}

// checkForFullyInst looks for fully-instantiated types in a type (at any nesting
// level). If it finds a fully-instantiated type, it ensures that the necessary
// dictionary and shape methods are exported. It updates p.checkFullyInst, so it
// traverses each particular type only once.
func (p *crawler) checkForFullyInst(t *types.Type) {
	if p.checkFullyInst[t] {
		return
	}
	p.checkFullyInst[t] = true

	if t.IsFullyInstantiated() && !t.HasShape() && !t.IsInterface() && t.Methods().Len() > 0 {
		// For any fully-instantiated type, the relevant
		// dictionaries and shape instantiations will have
		// already been created or are in the import data.
		// Make sure that they are exported, so that any
		// other package that inlines this function will have
		// them available for import, and so will not need
		// another round of method and dictionary
		// instantiation after inlining.
		baseType := t.OrigType()
		shapes := make([]*types.Type, len(t.RParams()))
		for i, t1 := range t.RParams() {
			shapes[i] = Shapify(t1, i, baseType.RParams()[i])
		}
		for j, tmethod := range t.Methods().Slice() {
			baseNname := baseType.Methods().Slice()[j].Nname.(*ir.Name)
			dictsym := MakeDictSym(baseNname.Sym(), t.RParams(), true)
			if dictsym.Def == nil {
				in := Resolve(ir.NewIdent(src.NoXPos, dictsym))
				dictsym = in.Sym()
			}
			Export(dictsym.Def.(*ir.Name))
			methsym := MakeFuncInstSym(baseNname.Sym(), shapes, false, true)
			if methsym.Def == nil {
				in := Resolve(ir.NewIdent(src.NoXPos, methsym))
				methsym = in.Sym()
			}
			methNode := methsym.Def.(*ir.Name)
			Export(methNode)
			if HaveInlineBody(methNode.Func) {
				// Export the body as well if
				// instantiation is inlineable.
				ImportedBody(methNode.Func)
				methNode.Func.SetExportInline(true)
			}
			// Make sure that any associated types are also exported. (See #52279)
			p.checkForFullyInst(tmethod.Type)
		}
	}

	// Descend into the type. We descend even if it is a fully-instantiated type,
	// since the instantiated type may have other instantiated types inside of
	// it (in fields, methods, etc.).
	switch t.Kind() {
	case types.TPTR, types.TARRAY, types.TSLICE:
		p.checkForFullyInst(t.Elem())

	case types.TCHAN:
		p.checkForFullyInst(t.Elem())

	case types.TMAP:
		p.checkForFullyInst(t.Key())
		p.checkForFullyInst(t.Elem())

	case types.TSTRUCT:
		if t.IsFuncArgStruct() {
			break
		}
		for _, f := range t.FieldSlice() {
			p.checkForFullyInst(f.Type)
		}

	case types.TFUNC:
		if recv := t.Recv(); recv != nil {
			p.checkForFullyInst(t.Recv().Type)
		}
		for _, f := range t.Params().FieldSlice() {
			p.checkForFullyInst(f.Type)
		}
		for _, f := range t.Results().FieldSlice() {
			p.checkForFullyInst(f.Type)
		}

	case types.TINTER:
		for _, f := range t.AllMethods().Slice() {
			p.checkForFullyInst(f.Type)
		}
	}
}

// markInlBody marks n's inline body for export and recursively
// ensures all called functions are marked too.
func (p *crawler) markInlBody(n *ir.Name) {
	if n == nil {
		return
	}
	if n.Op() != ir.ONAME || n.Class != ir.PFUNC {
		base.Fatalf("markInlBody: unexpected %v, %v, %v", n, n.Op(), n.Class)
	}
	fn := n.Func
	if fn == nil {
		base.Fatalf("markInlBody: missing Func on %v", n)
	}
	if !HaveInlineBody(fn) {
		return
	}

	if fn.ExportInline() {
		return
	}
	fn.SetExportInline(true)

	ImportedBody(fn)

	var doFlood func(n ir.Node)
	doFlood = func(n ir.Node) {
		t := n.Type()
		if t != nil {
			if t.HasTParam() {
				// If any generic types are used, then make sure that
				// the methods of the generic type are exported and
				// scanned for other possible exports.
				p.markGeneric(t)
			} else {
				p.checkForFullyInst(t)
			}
			if base.Debug.Unified == 0 {
				// If a method of un-exported type is promoted and accessible by
				// embedding in an exported type, it makes that type reachable.
				//
				// Example:
				//
				//     type t struct {}
				//     func (t) M() {}
				//
				//     func F() interface{} { return struct{ t }{} }
				//
				// We generate the wrapper for "struct{ t }".M, and inline call
				// to "struct{ t }".M, which makes "t.M" reachable.
				if t.IsStruct() {
					for _, f := range t.FieldSlice() {
						if f.Embedded != 0 {
							p.markEmbed(f.Type)
						}
					}
				}
			}
		}

		switch n.Op() {
		case ir.OMETHEXPR, ir.ODOTMETH:
			p.markInlBody(ir.MethodExprName(n))
		case ir.ONAME:
			n := n.(*ir.Name)
			switch n.Class {
			case ir.PFUNC:
				p.markInlBody(n)
				// Note: this Export() and the one below seem unneeded,
				// since any function/extern name encountered in an
				// exported function body will be exported
				// automatically via qualifiedIdent() in iexport.go.
				Export(n)
			case ir.PEXTERN:
				Export(n)
			}
		case ir.OMETHVALUE:
			// Okay, because we don't yet inline indirect
			// calls to method values.
		case ir.OCLOSURE:
			// VisitList doesn't visit closure bodies, so force a
			// recursive call to VisitList on the body of the closure.
			ir.VisitList(n.(*ir.ClosureExpr).Func.Body, doFlood)
		}
	}

	// Recursively identify all referenced functions for
	// reexport. We want to include even non-called functions,
	// because after inlining they might be callable.
	ir.VisitList(fn.Inl.Body, doFlood)
}

// isPtrFullyInstantiated returns true if t is a fully-instantiated type, or it is a
// pointer to a fully-instantiated type.
func isPtrFullyInstantiated(t *types.Type) bool {
	return t.IsPtr() && t.Elem().IsFullyInstantiated() ||
		t.IsFullyInstantiated()
}
