Merge branch 'dceIntegrationTests' of github.com:Workiva/gopherjs into dce4Instances
diff --git a/compiler/decls.go b/compiler/decls.go
index b6427a6..b55eecd 100644
--- a/compiler/decls.go
+++ b/compiler/decls.go
@@ -320,7 +320,7 @@
 		Blocking:    fc.pkgCtx.IsBlocking(o),
 		LinkingName: symbol.New(o),
 	}
-	d.Dce().SetName(o)
+	d.Dce().SetName(o, inst.TArgs...)
 
 	if typesutil.IsMethod(o) {
 		recv := typesutil.RecvType(o.Type().(*types.Signature)).Obj()
@@ -450,7 +450,7 @@
 
 	underlying := instanceType.Underlying()
 	d := &Decl{}
-	d.Dce().SetName(inst.Object)
+	d.Dce().SetName(inst.Object, inst.TArgs...)
 	fc.pkgCtx.CollectDCEDeps(d, func() {
 		// Code that declares a JS type (i.e. prototype) for each Go type.
 		d.DeclCode = fc.CatchOutput(0, func() {
diff --git a/compiler/expressions.go b/compiler/expressions.go
index dcf1b78..72c44b5 100644
--- a/compiler/expressions.go
+++ b/compiler/expressions.go
@@ -591,9 +591,7 @@
 		case types.MethodVal:
 			return fc.formatExpr(`$methodVal(%s, "%s")`, fc.makeReceiver(e), sel.Obj().(*types.Func).Name())
 		case types.MethodExpr:
-			if !sel.Obj().Exported() {
-				fc.pkgCtx.DeclareDCEDep(sel.Obj())
-			}
+			fc.pkgCtx.DeclareDCEDep(sel.Obj(), inst.TArgs...)
 			if _, ok := sel.Recv().Underlying().(*types.Interface); ok {
 				return fc.formatExpr(`$ifaceMethodExpr("%s")`, sel.Obj().(*types.Func).Name())
 			}
diff --git a/compiler/internal/dce/README.md b/compiler/internal/dce/README.md
new file mode 100644
index 0000000..4fe2882
--- /dev/null
+++ b/compiler/internal/dce/README.md
@@ -0,0 +1,600 @@
+# Dead-Code Elimination
+
+Dead-Code Eliminations (DCE) is used to remove code that isn't
+reachable from a code entry point. Entry points are code like the main method,
+init functions, and variable initializations with side effects.
+These entry points are always considered alive. Any dependency of
+something alive, is also considered alive.
+
+Once all dependencies are taken into consideration we have the set of alive
+declarations. Anything not considered alive is considered dead and
+may be safely eliminated, i.e. not outputted to the JS file(s).
+
+- [Idea](#idea)
+  - [Package](#package)
+  - [Named Types](#named-types)
+    - [Named Structs](#named-structs)
+  - [Interfaces](#interfaces)
+  - [Functions](#functions)
+  - [Variables](#variables)
+  - [Generics and Instances](#generics-and-instances)
+  - [Links](#links)
+- [Design](#design)
+  - [Initially Alive](#initially-alive)
+  - [Naming](#naming)
+    - [Name Specifics](#name-specifics)
+  - [Dependencies](#dependencies)
+- [Examples](#examples)
+  - [Dead Package](#dead-package)
+  - [Grandmas and Zombies](#grandmas-and-zombies)
+  - [Side Effects](#side-effects)
+  - [Instance Duck-typing](#instance-duck-typing)
+- [Additional Notes](#additional-notes)
+
+## Idea
+
+The following is the logic behind the DCE mechanism. Not all of the following
+is used since some conditions are difficult to determine even with a lot of
+additional information. To ensure that the JS output is fully functional,
+we bias the DCE towards things being alive. We'd rather keep something we
+don't need than remove something that is needed.
+
+### Package
+
+Package declarations (e.g. `package foo`) might be able to be removed
+when only used by dead-code. However, packages may be imported and not used
+for various reasons including to invoke some initialization or to implement
+a link. So it is difficult to determine.
+See [Dead Package](#dead-package) example.
+
+Currently, we won't remove any packages, but someday the complexity
+could be added to check for inits, side effects, links, etc then determine
+if any of those are are alive or affect alive things.
+
+### Named Types
+
+Named type definitions (e.g. `type Foo int`) depend on
+the underlying type for each definition.
+
+When a named type is alive, all of its exported methods
+(e.g. `func (f Foo) Bar() { }`) are also alive, even any unused exported method.
+Unused exported methods are still important when duck-typing.
+See [Interfaces](#interfaces) for more information.
+See [Grandmas and Zombies](#grandmas-and-zombies) for an example of what
+can happen when removing an unused exported method.
+
+Also unused exported methods could be accessed by name via reflect
+(e.g. `reflect.ValueOf(&Foo{}).MethodByName("Bar")`). Since the
+string name may be provided from outside the code, such as the command line,
+it is impossible to determine which exported methods could be accessed this way.
+It would be very difficult to determine which types are ever accessed via
+reflect so by default we simply assume any can be.
+
+Methods that are unexported may be considered dead when unused even when
+the receiver type is alive. The exception is when an interface in the same
+package has the same unexported method in it.
+See [Interfaces](#interfaces) for more information.
+
+#### Named Structs
+
+A named struct is a named type that has a struct as its underlying type,
+e.g. `type Foo struct { }`. A struct type depends on all of the types in
+its fields and embedded fields.
+
+If the struct type is alive then all the types of the fields will also be alive.
+Even unexported fields maybe accessed via reflections, so they all must be
+alive. Also, the fields are needed for comparisons and serializations
+(such as `encoding/binary`).
+
+### Interfaces
+
+All the types in the function signatures and embedded interfaces are the
+dependents of the interface.
+
+Interfaces may contain exported and unexported function signatures.
+If an interface is alive then all of the functions, even the unexported
+functions, are alive.
+Since there are many ways to wrap a type with an interface, any alive type that
+duck-types to an interface must have all of the matching methods alive.
+
+Since the exported methods in an alive type will be alive, see
+[Named Types](#named-types), the only ones here that need to be considered
+are the unexported methods. An interface with unexported methods may only
+duck-type to types within the package the interface is defined in.
+Therefore, if an interface is alive with unexported methods, then all
+alive types within the same package that duck-type to that interface,
+will have the matching unexported methods be alive.
+
+Since doing a full `types.Implements` check between every named types and
+interfaces in a package is difficult, we simplify this requirement to be
+any unexported method in an alive named type that matches an unexported
+method in an alive interface is alive even if the named type doesn't duck-type
+to the interface. This means that in some rare cases, some unexported
+methods on named structs that could have been eliminated will not be.
+For example, given `type Foo struct{}; func(f Foo) X(); func (f Foo) y()` the
+`Foo.y()` method may be alive if `types Bar interface { Z(); y() }` is alive
+even though the `X()` and `Z()` means that `Foo` doesn't implement `Bar`
+and therefore `Foo.y()` can not be called via a `Bar.y()`.
+
+We will try to reduce the false positives in alive unexported methods by using
+the parameter and result types of the methods. Meaning that
+ `y()`, `y(int)`, `y() int`, etc won't match just because they are named `y`.
+
+### Functions
+
+Functions with or without a receiver are dependent on the types used by the
+parameters, results, and type uses inside the body of the function.
+They are also dependent on any function invoked or used, and
+any package level variable that is used.
+
+Unused functions without a receiver, that are exported or not, may be
+considered dead since they aren't used in duck-typing and cannot be accessed
+by name via reflections.
+
+### Variables
+
+Variables (or constants) depend on their type and anything used during
+initialization.
+
+The exported or unexported variables are dead unless they are used by something
+else that is alive or if the initialization has side effects.
+
+If the initialization has side effects the variable will be alive even
+if unused. The side effect may be simply setting another variable's value
+that is also unused, however it would be difficult to determine if the
+side effects are used or not.
+See [Side Effects](#side-effects) example.
+
+### Generics and Instances
+
+For functions and types with generics, the definitions are split into
+unique instances. For example, `type StringKeys[T any] map[string]T`
+could be used in code as `StringKeys[int]` and `StringKeys[*Cat]`.
+We don't need all possible instances, only the ones which are realized
+in code. Each instance depends on the realized parameter types (instance types).
+In the example the instance types are `int` and `*Cat`.
+
+The instance of the generic type also defines the code with the specific
+instance types (e.g. `map[string]int` and `map[string]*Cat`). When an
+instance is depended on by alive code, only that instance is alive, not the
+entire generic type. This means if `StringKey[*Cat]` is only used from dead
+code, it is also dead and can be safely eliminated.
+
+The named generic types may have methods that are also copied for an instance
+with the parameter types replaced by the instance types. For example,
+`func (sk StringKeys[T]) values() []T { ... }` becomes
+`func (sk StringKeys[int]) values() []int { ... }` when the instance type
+is `int`. This method in the instance now duck-types to
+`interface { values() []int }` and therefore must follow the rules for
+unexported methods.
+See [Instance Duck-typing](#instance-duck-typing) example for more information.
+
+Functions and named types may be generic, but methods and unnamed types
+may not be. This makes somethings simpler. A method with a receiver is used,
+only the receiver's instance types are needed. The generic type or function
+may not be needed since only the instances are written out.
+
+This also means that inside of a generic function or named type there is only
+one type parameter list being used. Even generic types used inside of the
+generic function must be specified in terms of the type parameter for the
+generic and doesn't contribute any type parameters of it's own.
+For example, inside of `func Foo[K comparable, V any]() { ... }` every
+usage of a generic type must specify a concrete type (`int`, `*Cat`,
+`Bar[Bar[bool]]`) or use the parameter types `K` and `V`. This is simpler
+than languages that allow a method of an object to have it's own type
+parameters, e.g. `class X<T> { void Y<U>() { ... } ... }`.
+
+However, generics mean that the same method, receiver, type, etc names
+will be used with different parameters types caused by different instance
+types. The instance types are the type arguments being passed into those
+parameter types for a specific instance.
+When an interface is alive, the signatures for unexported methods
+need to be instantiated with type arguments so that we know which instances
+the interface is duck-typing to.
+
+### Links
+
+Links use compiler directives
+([`//go:linkname`](https://pkg.go.dev/cmd/compile#hdr-Compiler_Directives))
+to alias a `var` or `func` with another.
+For example some code may have `func bar_foo()` as a function stub that is
+linked with `foo() { ... }` as a function with a body, i.e. the target of the
+link. The links are single directional but allow multiple stubs to link to the
+same target.
+
+When a link is made, the dependencies for the linked code come from
+the target. If the target is used by something alive then it is alive.
+If a stub linked to a target is used by something alive then that stub and
+the target are both alive.
+
+Since links cross package boundaries in ways that may violate encapsulation
+and the dependency tree, it may be difficult to determine if a link is alive
+or not. Therefore, currently all links are considered alive.
+
+## Design
+
+The design is created taking all the parts of the above idea together and
+simplifying the justifications down to a simple set of rules.
+
+### Initially alive
+
+- The `main` method in the `main` package
+- The `init` in every included file
+- Any variable initialization that has a side effect
+- Any linked function or variable
+- Anything not named
+
+### Naming
+
+The following specifies what declarations should be named and how
+the names should look. These names are later used to match (via string
+comparisons) dependencies with declarations that should be set as alive.
+Since the names are used to filter out alive code from all the code
+these names may also be referred to as filters.
+
+Some names will have multiple name parts; an object name and method name.
+This is kind of like a first name and last name when a first name alone isn't
+specific enough. This helps with matching multiple dependency requirements
+for a declaration, i.e. both name parts must be alive before the declaration
+is considered alive.
+
+Currently, only unexported method declarations will have a method
+name to support duck-typing with unexported signatures on interfaces.
+If the unexported method is depended on, then both names will be in
+the dependencies. If the receiver is alive and an alive interface has the
+matching unexported signature, then both names will be depended on thus making
+the unexported method alive. Since the unexported method is only visible in
+the package in which it is defined, the package path is included in the
+method name.
+
+| Declaration | exported | unexported | non-generic | generic | object name | method name |
+|:------------|:--------:|:----------:|:-----------:|:-------:|:------------|:------------|
+| variables  | █ | █ | █ | n/a | `<package>.<var name>` | |
+| functions  | █ | █ | █ |     | `<package>.<func name>` | |
+| functions  | █ | █ |   |  █  | `<package>.<func name>[<type args>]` | |
+| named type | █ | █ | █ |     | `<package>.<type name>` | |
+| named type | █ | █ |   |  █  | `<package>.<type name>[<type args>]` | |
+| method     | █ |   | █ |     | `<package>.<receiver name>` | |
+| method     | █ |   |   |  █  | `<package>.<receiver name>[<type args>]` | |
+| method     |   | █ | █ |     | `<package>.<receiver name>` | `<package>.<method name>(<parameter types>)(<result types>)` |
+| method     |   | █ |   |  █  | `<package>.<receiver name>[<type args>]` | `<package>.<method name>(<parameter types>)(<result types>)` |
+
+#### Name Specifics
+
+The following are specifics about the different types of names that show
+up in the above table. This isn't the only way to represent this information.
+These names can get long but don't have to. The goal is to make the names
+as unique as possible whilst still ensuring that signatures in
+interfaces will still match the correct methods. The less unique
+the more false positives for alive will occur meaning more dead code is
+kept alive. However, too unique could cause needed alive code to not match
+and be eliminated causing the application to not run.
+
+`<package>.<var name>`, `<package>.<func name>`, `<package>.<type name>`
+and `<package>.<receiver name>` all have the same form. They are
+the package path, if there is one, followed by a `.` and the object name
+or receiver name. For example [`rand.Shuffle`](https://pkg.go.dev/math/rand@go1.23.1#Shuffle)
+will be named `math/rand.Shuffle`. The builtin [`error`](https://pkg.go.dev/builtin@go1.23.1#error)
+will be named `error` without a package path.
+
+`<package>.<func name>[<type args>]`, `<package>.<type name>[<type args>]`,
+and `<package>.<receiver name>[<type args>]` are the same as above
+except with comma separated type arguments in square brackets.
+The type arguments are either the instance types, or type parameters
+since the instance type could be a match for the type parameter on the
+generic. For example `type Foo[T any] struct{}; type Bar[B any] { f Foo[B] }`
+has `Foo[B]` used in `Bar` that is identical to `Foo[T]` even though
+technically `Foo[B]` is an instance of `Foo[T]` with `B` as the type argument.
+
+Command compiles, i.e. compiles with a `main` entry point, and test builds
+should not have any instance types that aren't resolved to concrete types,
+however to handle partial compiles of packages, instance types may still
+be a type parameter, including unions of approximate constraints,
+i.e. `~int|~string`.
+
+Therefore, type arguments need to be reduced to only types. This means
+something like [`maps.Keys`](https://pkg.go.dev/maps@go1.23.1#Keys), i.e.
+`func Keys[Map ~map[K]V, K comparable, V any](m Map) iter.Seq[K]`,
+will be named `maps.Keys[~map[comparable]any, comparable, any]` as a generic.
+If the instances for `Map` are `map[string]int` and `map[int][]*cats.Cat`,
+then respectively the names would be `maps.Keys[map[string]int, string, int]`
+and `maps.Keys[map[int][]*cats.Cat, int, []*cats.Cat]`. If this function is used
+in `func Foo[T ~string|~int](data map[string]T) { ... maps.Keys(data) ... }`
+then the instance of `maps.Keys` that `Foo` depends on would be named
+`maps.Keys[map[string]~int|~string, string, ~int|~string]`.
+
+For the method name of unexposed methods,
+`<package>.<method name>(<parameter types>)(<result types>)`, the prefix,
+`<package>.<method name>`, is in the same format as `<package>.<func name>`.
+The rest contains the signature, `(<parameter types>)(<result types>)`.
+The signature is defined with only the types since
+`(v, u int)(ok bool, err error)` should match `(x, y int)(bool, error)`.
+To match both will have to be `(int, int)(bool, error)`.
+Also the parameter types should include the veridic indicator,
+e.g. `sum(...int)int`, since that affects how the signature is matched.
+If there are no results then the results part is left off. Otherwise,
+the result types only need parenthesis if there are more than one result,
+e.g. `(int, int)`, `(int, int)bool`, and `(int, int)(bool, error)`.
+
+In either the object name or method name, if there is a recursive
+type parameter, e.g. `func Foo[T Bar[T]]()` the second usage of the
+type parameter will have it's type parameters as `...` to prevent an
+infinite loop whilst also indicating which object in the type parameter
+is recursive, e.g. `Foo[Bar[Bar[...]]]`.
+
+### Dependencies
+
+The dependencies are initialized via two paths.
+
+The first is dependencies that are specified in an expression.
+For example a function that invokes another function will be dependent on
+that invoked function. When a dependency is added it will be added as one
+or more names to the declaration that depends on it. It follows the
+[naming rules](#naming) so that the dependencies will match correctly.
+
+The second is structural dependencies that are specified automatically while
+the declaration is being named. When an interface is named, it will
+automatically add all unexported signatures as dependencies via
+`<package path>.<method name>(<parameter type list>)(<result type list>)`.
+
+Currently we don't filter unused packages so there is no need to automatically
+add dependencies on the packages themselves. This is also why the package
+declarations aren't named and therefore are always alive.
+
+## Examples
+
+### Dead Package
+
+In this example, a point package defines a `Point` object.
+The point package may be used by several repos as shared code so can not
+have code manually removed from it to reduce its dependencies for specific
+applications.
+
+For the current example, the `Distance` method is never used and therefore
+dead. The `Distance` method is the only method dependent on the math package.
+It might be safe to make the whole math package dead too and eliminate it in
+this case, however, it is possible that some packages aren't used on purpose
+and their reason for being included is to invoke the initialization functions
+within the package. If a package has any inits or any variable definitions
+with side effects, then the package can not be safely removed.
+
+```go
+package point
+
+import "math"
+
+type Point struct {
+   X float64
+   Y float64
+}
+
+func (p Point) Sub(other Point) Point {
+   p.X -= other.X
+   p.Y -= other.Y
+   return p
+}
+
+func (p Point) ToQuadrant1() Point {
+   if p.X < 0.0 {
+      p.X = -p.X
+   }
+   if p.Y < 0.0 {
+      p.Y = -p.Y
+   }
+   return p
+}
+
+func (p Point) Manhattan(other Point) float64 {
+   a := p.Sub(other).ToQuadrant1()
+   return a.X + a.Y
+}
+
+func (p Point) Distance(other Point) float64 {
+   d := p.Sub(other)
+   return math.Sqrt(d.X*d.X + d.Y*d.Y)
+}
+```
+
+```go
+package main
+
+import "point"
+
+func main() {
+   a := point.Point{X: 10.2, Y: 45.3}
+   b := point.Point{X: -23.0, Y: 7.7}
+   println(`Manhatten a to b:`, a.Manhattan(b))
+}
+```
+
+### Grandmas and Zombies
+
+In this example, the following code sorts grandmas and zombies by if they are
+`Dangerous`. The method `EatBrains` is never used. If we remove `EatBrains`
+from `Zombie` then both the grandmas and zombies are moved to the safe
+bunker. If we remove `EatBrains` from `Dangerous` then both grandmas and
+zombies will be moved to the air lock because `Dangerous` will duck-type
+to all `Person` instances. Unused exported methods and signatures must be
+considered alive if the type is alive.
+
+```go
+package main
+
+import "fmt"
+
+type Person interface {
+   MoveTo(loc string)
+}
+
+type Dangerous interface {
+   Person
+   EatBrains()
+}
+
+type Grandma struct{}
+
+func (g Grandma) MoveTo(loc string) {
+   fmt.Println(`grandma was moved to`, loc)
+}
+
+type Zombie struct{}
+
+func (z Zombie) MoveTo(loc string) {
+   fmt.Println(`zombie was moved to`, loc)
+}
+
+func (z Zombie) EatBrains() {}
+
+func main() {
+   people := []Person{Grandma{}, Zombie{}, Grandma{}, Zombie{}}
+   for _, person := range people {
+      if _, ok := person.(Dangerous); ok {
+         person.MoveTo(`air lock`)
+      } else {
+         person.MoveTo(`safe bunker`)
+      }
+   }
+}
+```
+
+### Side Effects
+
+In this example unused variables are being initialized with expressions
+that have side effects. The `max` value is 8 by the time `main` is called
+because each initialization calls `count()` that increments `max`.
+The expression doesn't have to have a function call and can be any combination
+of operations.
+
+An initialization may have a side effect even if it doesn't set a value. For
+example, simply printing a message to the console is a side effect that
+can not be removed even if it is part of an unused initializer.
+
+```go
+package main
+
+import "fmt"
+
+func count() int {
+   max++
+   return max
+}
+
+var (
+   max  = 0
+   _    = count() // a
+   b, c = count(), count()
+   x    = []int{count(), count(), count()}[0]
+   y, z = func() (int, int) { return count(), count() }()
+)
+
+func main() {
+   fmt.Println(`max count`, max) // Outputs: max count 8
+}
+```
+
+### Instance Duck-typing
+
+In this example the type `StringKeys[T any]` is a map that stores
+any kind of value with string keys. There is an interface `IntProvider`
+that `StringKeys` will duck-type to iff the instance type is `int`,
+i.e. `StringKeys[int]`. This exemplifies how the instance types used
+in the type arguments affect the overall signature such that in some
+cases a generic object may match an interface and in others it may not.
+
+Also notice that the structure was typed with `T` as the parameter type's
+name whereas the methods use `S`. This shows that the name of the type
+doesn't matter in the instancing. Therefore, outputting a methods name
+(assuming it is unexported) should use the instance type not the parameter
+name, e.g. `value() []int` or `value() []any` instead of `value() []S` or
+`value() []T`.
+
+```go
+package main
+
+import (
+   "fmt"
+   "sort"
+)
+
+type StringKeys[T any] map[string]T
+
+func (sk StringKeys[S]) Keys() []string {
+   keys := make([]string, 0, len(sk))
+   for key := range sk {
+      keys = append(keys, key)
+   }
+   sort.Strings(keys)
+   return keys
+}
+
+func (sk StringKeys[S]) Values() []S {
+   values := make([]S, len(sk))
+   for i, key := range sk.Keys() {
+      values[i] = sk[key]
+   }
+   return values
+}
+
+type IntProvider interface {
+   Values() []int
+}
+
+func Sum(data IntProvider) int {
+   sum := 0
+   for _, value := range data.Values() {
+      sum += value
+   }
+   return sum
+}
+
+func main() {
+   sInt := StringKeys[int]{
+      `one`:   1,
+      `two`:   2,
+      `three`: 3,
+      `four`:  4,
+   }
+   fmt.Println(sInt.Keys())   // Outputs: [four one three two]
+   fmt.Println(sInt.Values()) // Outputs: [4 1 3 2]
+   fmt.Println(Sum(sInt))     // Outputs: 10
+
+   sFp := StringKeys[float64]{
+      `one`:   1.1,
+      `two`:   2.2,
+      `three`: 3.3,
+      `four`:  4.4,
+   }
+   fmt.Println(sFp.Keys())   // Outputs: [four one three two]
+   fmt.Println(sFp.Values()) // [4.4 1.1 3.3 2.2]
+   //fmt.Println(Sum(sFp))   // Fails with "StringKeys[float64] does not implement IntProvider"
+}
+```
+
+## Additional Notes
+
+This DCE is different from those found in
+Muchnick, Steven S.. “Advanced Compiler Design and Implementation.” (1997),
+Chapter 18 Control-Flow and Low-Level Optimization,
+Section 10 Dead-Code Elimination. And different from related DCE designs
+such as Knoop, Rüthing, and Steffen. "Partial dead code elimination." (1994),
+SIGPLAN Not. 29, 6, 147–158.
+See [DCE wiki](https://en.wikipedia.org/wiki/Dead-code_elimination)
+for more information.
+
+Those discuss DCE at the block code level where the higher level
+constructs such as functions and objects have been reduced to a graphs of
+blocks with variables, procedures, and routines. Since we want to keep the
+higher level constructs during transpilation, we simply are reducing
+the higher level constructs not being used.
+
+Any variable internal to the body of a function or method that is unused or
+only used for computing new values for itself, are left as is.
+The Go compiler and linters have requirements that attempt to prevent this
+kind of dead-code in a function body (so long as an underscore isn't used to quite
+usage warnings) and prevent unreachable code. Therefore, we aren't going to
+worry about trying to DCE inside of function bodies or in variable initializers.
+
+GopherJS does not implicitly perform JS Tree Shaking Algorithms, as discussed in
+[How Modern Javascript eliminate dead code](https://blog.stackademic.com/how-modern-javascript-eliminates-dead-code-tree-shaking-algorithm-d7861e48df40)
+(2023) at this time and provides no guarantees about the effectiveness
+of running such an algorithm on the resulting JS.
diff --git a/compiler/internal/dce/collector.go b/compiler/internal/dce/collector.go
index 7d25102..fea5246 100644
--- a/compiler/internal/dce/collector.go
+++ b/compiler/internal/dce/collector.go
@@ -14,7 +14,7 @@
 // Collector is a tool to collect dependencies for a declaration
 // that'll be used in dead-code elimination (DCE).
 type Collector struct {
-	dependencies map[types.Object]struct{}
+	dce *Info
 }
 
 // CollectDCEDeps captures a list of Go objects (types, functions, etc.)
@@ -22,25 +22,25 @@
 // as dependencies of the given dead-code elimination info.
 //
 // Only one CollectDCEDeps call can be active at a time.
-// This will overwrite any previous dependencies collected for the given DCE.
 func (c *Collector) CollectDCEDeps(decl Decl, f func()) {
-	if c.dependencies != nil {
+	if c.dce != nil {
 		panic(errors.New(`called CollectDCEDeps inside another CollectDCEDeps call`))
 	}
 
-	c.dependencies = make(map[types.Object]struct{})
-	defer func() { c.dependencies = nil }()
+	c.dce = decl.Dce()
+	defer func() { c.dce = nil }()
 
 	f()
-
-	decl.Dce().setDeps(c.dependencies)
 }
 
 // DeclareDCEDep records that the code that is currently being transpiled
-// depends on a given Go object.
-func (c *Collector) DeclareDCEDep(o types.Object) {
-	if c.dependencies == nil {
-		return // Dependencies are not being collected.
+// depends on a given Go object with optional type arguments.
+//
+// The given optional type arguments are used to when the object is a
+// function with type parameters or anytime the object doesn't carry them.
+// If not given, this attempts to get the type arguments from the object.
+func (c *Collector) DeclareDCEDep(o types.Object, tArgs ...types.Type) {
+	if c.dce != nil {
+		c.dce.addDep(o, tArgs)
 	}
-	c.dependencies[o] = struct{}{}
 }
diff --git a/compiler/internal/dce/dce_test.go b/compiler/internal/dce/dce_test.go
index c46a7f0..226e90c 100644
--- a/compiler/internal/dce/dce_test.go
+++ b/compiler/internal/dce/dce_test.go
@@ -10,6 +10,8 @@
 	"regexp"
 	"sort"
 	"testing"
+
+	"github.com/gopherjs/gopherjs/compiler/typesutil"
 )
 
 func Test_Collector_CalledOnce(t *testing.T) {
@@ -65,20 +67,20 @@
 	depCount(t, decl1, 2)
 	depCount(t, decl2, 3)
 
-	// The second collection overwrites the first collection.
+	// The second collection adds to existing dependencies.
 	c.CollectDCEDeps(decl2, func() {
+		c.DeclareDCEDep(obj4)
 		c.DeclareDCEDep(obj5)
 	})
 	depCount(t, decl1, 2)
-	depCount(t, decl2, 1)
+	depCount(t, decl2, 4)
 }
 
 func Test_Info_SetNameAndDep(t *testing.T) {
 	tests := []struct {
-		name    string
-		obj     types.Object
-		want    Info   // expected Info after SetName
-		wantDep string // expected dep after addDep
+		name string
+		obj  types.Object
+		want Info // expected Info after SetName
 	}{
 		{
 			name: `package`,
@@ -86,32 +88,26 @@
 				`package jim
 				import Sarah "fmt"`),
 			want: Info{
-				importPath:   `jim`,
-				objectFilter: `Sarah`,
+				objectFilter: `jim.Sarah`,
 			},
-			wantDep: `jim.Sarah`,
 		},
 		{
-			name: `exposed var`,
+			name: `exported var`,
 			obj: parseObject(t, `Toby`,
 				`package jim
 				var Toby float64`),
 			want: Info{
-				importPath:   `jim`,
-				objectFilter: `Toby`,
+				objectFilter: `jim.Toby`,
 			},
-			wantDep: `jim.Toby`,
 		},
 		{
-			name: `exposed const`,
+			name: `exported const`,
 			obj: parseObject(t, `Ludo`,
 				`package jim
 				const Ludo int = 42`),
 			want: Info{
-				importPath:   `jim`,
-				objectFilter: `Ludo`,
+				objectFilter: `jim.Ludo`,
 			},
-			wantDep: `jim.Ludo`,
 		},
 		{
 			name: `label`,
@@ -126,91 +122,487 @@
 					}
 				}`),
 			want: Info{
-				importPath:   `jim`,
-				objectFilter: `Gobo`,
+				objectFilter: `jim.Gobo`,
 			},
-			wantDep: `jim.Gobo`,
 		},
 		{
-			name: `exposed specific type`,
+			name: `exported specific type`,
 			obj: parseObject(t, `Jen`,
 				`package jim
 				type Jen struct{}`),
 			want: Info{
-				importPath:   `jim`,
-				objectFilter: `Jen`,
+				objectFilter: `jim.Jen`,
 			},
-			wantDep: `jim.Jen`,
 		},
 		{
-			name: `exposed generic type`,
+			name: `exported generic type`,
 			obj: parseObject(t, `Henson`,
 				`package jim
 				type Henson[T comparable] struct{}`),
 			want: Info{
-				importPath:   `jim`,
-				objectFilter: `Henson`,
+				objectFilter: `jim.Henson[comparable]`,
 			},
-			wantDep: `jim.Henson`,
 		},
 		{
-			name: `exposed specific function`,
+			name: `exported specific function`,
 			obj: parseObject(t, `Jareth`,
 				`package jim
 				func Jareth() {}`),
 			want: Info{
-				importPath:   `jim`,
-				objectFilter: `Jareth`,
+				objectFilter: `jim.Jareth`,
 			},
-			wantDep: `jim.Jareth`,
 		},
 		{
-			name: `exposed generic function`,
+			name: `exported generic function`,
 			obj: parseObject(t, `Didymus`,
 				`package jim
 				func Didymus[T comparable]() {}`),
 			want: Info{
-				importPath:   `jim`,
-				objectFilter: `Didymus`,
+				objectFilter: `jim.Didymus[comparable]`,
 			},
-			wantDep: `jim.Didymus`,
 		},
 		{
-			name: `exposed specific method`,
+			name: `exported specific method`,
 			obj: parseObject(t, `Kira`,
 				`package jim
 				type Fizzgig string
 				func (f Fizzgig) Kira() {}`),
 			want: Info{
-				importPath:   `jim`,
-				objectFilter: `Fizzgig`,
+				objectFilter: `jim.Fizzgig`,
 			},
-			wantDep: `jim.Kira~`,
 		},
 		{
-			name: `unexposed specific method`,
+			name: `unexported specific method without parameters or results`,
 			obj: parseObject(t, `frank`,
 				`package jim
 				type Aughra int
 				func (a Aughra) frank() {}`),
 			want: Info{
-				importPath:   `jim`,
-				objectFilter: `Aughra`,
-				methodFilter: `frank~`,
+				objectFilter: `jim.Aughra`,
+				methodFilter: `jim.frank()`,
 			},
-			wantDep: `jim.frank~`,
 		},
 		{
-			name: `specific method on unexposed type`,
+			name: `unexported specific method with parameters and results`,
+			obj: parseObject(t, `frank`,
+				`package jim
+				type Aughra int
+				func (a Aughra) frank(other Aughra) (bool, error) {
+					return a == other, nil
+				}`),
+			want: Info{
+				objectFilter: `jim.Aughra`,
+				methodFilter: `jim.frank(jim.Aughra)(bool, error)`,
+			},
+		},
+		{
+			name: `unexported specific method with variadic parameter`,
+			obj: parseObject(t, `frank`,
+				`package jim
+				type Aughra int
+				func (a Aughra) frank(others ...Aughra) int {
+					return len(others) + 1
+				}`),
+			want: Info{
+				objectFilter: `jim.Aughra`,
+				methodFilter: `jim.frank(...jim.Aughra) int`,
+			},
+		},
+		{
+			name: `unexported generic method with type parameters and instance argument`,
+			obj: parseObject(t, `frank`,
+				`package jim
+				type Aughra[T ~float64] struct {
+					value T
+				}
+				func (a *Aughra[T]) frank(other *Aughra[float64]) bool {
+					return float64(a.value) == other.value
+				}`),
+			want: Info{
+				objectFilter: `jim.Aughra[~float64]`,
+				methodFilter: `jim.frank(*jim.Aughra[float64]) bool`,
+			},
+		},
+		{
+			name: `unexported generic method with type parameters and generic argument`,
+			obj: parseObject(t, `frank`,
+				`package jim
+				type Aughra[T ~float64] struct {
+					value T
+				}
+				func (a *Aughra[T]) frank(other *Aughra[T]) bool {
+					return a.value == other.value
+				}`),
+			want: Info{
+				objectFilter: `jim.Aughra[~float64]`,
+				methodFilter: `jim.frank(*jim.Aughra[~float64]) bool`,
+			},
+		},
+		{
+			name: `specific method on unexported type`,
 			obj: parseObject(t, `Red`,
 				`package jim
 				type wembley struct{}
 				func (w wembley) Red() {}`),
 			want: Info{
-				importPath:   `jim`,
-				objectFilter: `wembley`,
+				objectFilter: `jim.wembley`,
 			},
-			wantDep: `jim.Red~`,
+		},
+		{
+			name: `interface with unexported methods setting dependencies`,
+			obj: parseObject(t, `Hoggle`,
+				`package jim
+				type Hoggle interface{
+					cowardly() bool
+					loyalTo(goblin string) bool
+					makePrinceOfTheBogOfEternalStench() error
+				}`),
+			want: Info{
+				objectFilter: `jim.Hoggle`,
+				// The automatically defined dependencies for unexported methods
+				// in the interface that match with the methodFilter of unexported methods.
+				deps: map[string]struct{}{
+					`jim.cowardly() bool`:                           {},
+					`jim.loyalTo(string) bool`:                      {},
+					`jim.makePrinceOfTheBogOfEternalStench() error`: {},
+				},
+			},
+		},
+		{
+			name: `unexported method resulting in an interface with exported methods`,
+			obj: parseObject(t, `bear`,
+				`package jim
+				type Fozzie struct{}
+				func (f *Fozzie) bear() interface{
+					WakkaWakka(joke string)(landed bool)
+					Firth()(string, error)
+				}`),
+			want: Info{
+				objectFilter: `jim.Fozzie`,
+				methodFilter: `jim.bear() interface{ Firth()(string, error); WakkaWakka(string) bool }`,
+			},
+		},
+		{
+			name: `unexported method resulting in an interface with unexported methods`,
+			obj: parseObject(t, `bear`,
+				`package jim
+				type Fozzie struct{}
+				func (f *Fozzie) bear() interface{
+					wakkaWakka(joke string)(landed bool)
+					firth()(string, error)
+				}`),
+			want: Info{
+				objectFilter: `jim.Fozzie`,
+				// The package path, i.e. `jim.`, is used on unexported methods
+				// to ensure the filter will not match another package's method.
+				methodFilter: `jim.bear() interface{ jim.firth()(string, error); jim.wakkaWakka(string) bool }`,
+			},
+		},
+		{
+			name: `unexported method resulting in an empty interface `,
+			obj: parseObject(t, `bear`,
+				`package jim
+				type Fozzie struct{}
+				func (f *Fozzie) bear() interface{}`),
+			want: Info{
+				objectFilter: `jim.Fozzie`,
+				methodFilter: `jim.bear() any`,
+			},
+		},
+		{
+			name: `unexported method resulting in a function`,
+			obj: parseObject(t, `bear`,
+				`package jim
+				type Fozzie struct{}
+				func (f *Fozzie) bear() func(joke string)(landed bool)`),
+			want: Info{
+				objectFilter: `jim.Fozzie`,
+				methodFilter: `jim.bear() func(string) bool`,
+			},
+		},
+		{
+			name: `unexported method resulting in a struct`,
+			obj: parseObject(t, `bear`,
+				`package jim
+				type Fozzie struct{}
+				func (f *Fozzie) bear() struct{
+					Joke string
+					WakkaWakka bool
+				}`),
+			want: Info{
+				objectFilter: `jim.Fozzie`,
+				methodFilter: `jim.bear() struct{ Joke string; WakkaWakka bool }`,
+			},
+		},
+		{
+			name: `unexported method resulting in a struct with type parameter`,
+			obj: parseObject(t, `bear`,
+				`package jim
+				type Fozzie[T ~string|~int] struct{}
+				func (f *Fozzie[T]) bear() struct{
+					Joke T
+					wakkaWakka bool
+				}`),
+			want: Info{
+				objectFilter: `jim.Fozzie[~int|~string]`,
+				// The `Joke ~int|~string` part will likely not match other methods
+				// such as methods with `Joke string` or `Joke int`, however the
+				// interface should be defined for the instantiations of this type
+				// and those should have the correct field type for `Joke`.
+				methodFilter: `jim.bear() struct{ Joke ~int|~string; jim.wakkaWakka bool }`,
+			},
+		},
+		{
+			name: `unexported method resulting in an empty struct`,
+			obj: parseObject(t, `bear`,
+				`package jim
+				type Fozzie struct{}
+				func (f *Fozzie) bear() struct{}`),
+			want: Info{
+				objectFilter: `jim.Fozzie`,
+				methodFilter: `jim.bear() struct{}`,
+			},
+		},
+		{
+			name: `unexported method resulting in a slice`,
+			obj: parseObject(t, `bear`,
+				`package jim
+				type Fozzie struct{}
+				func (f *Fozzie) bear()(jokes []string)`),
+			want: Info{
+				objectFilter: `jim.Fozzie`,
+				methodFilter: `jim.bear() []string`,
+			},
+		},
+		{
+			name: `unexported method resulting in an array`,
+			obj: parseObject(t, `bear`,
+				`package jim
+				type Fozzie struct{}
+				func (f *Fozzie) bear()(jokes [2]string)`),
+			want: Info{
+				objectFilter: `jim.Fozzie`,
+				methodFilter: `jim.bear() [2]string`,
+			},
+		},
+		{
+			name: `unexported method resulting in a map`,
+			obj: parseObject(t, `bear`,
+				`package jim
+				type Fozzie struct{}
+				func (f *Fozzie) bear()(jokes map[string]bool)`),
+			want: Info{
+				objectFilter: `jim.Fozzie`,
+				methodFilter: `jim.bear() map[string]bool`,
+			},
+		},
+		{
+			name: `unexported method resulting in a channel`,
+			obj: parseObject(t, `bear`,
+				`package jim
+				type Fozzie struct{}
+				func (f *Fozzie) bear() chan string`),
+			want: Info{
+				objectFilter: `jim.Fozzie`,
+				methodFilter: `jim.bear() chan string`,
+			},
+		},
+		{
+			name: `unexported method resulting in a complex compound named type`,
+			obj: parseObject(t, `packRat`,
+				`package jim
+				type Gonzo[T any] struct{
+					v T
+				}
+				func (g Gonzo[T]) Get() T { return g.v }
+				type Rizzo struct{}
+				func (r Rizzo) packRat(v int) Gonzo[Gonzo[Gonzo[int]]] {
+					return Gonzo[Gonzo[Gonzo[int]]]{v: Gonzo[Gonzo[int]]{v: Gonzo[int]{v: v}}}
+				}
+				var _ int = Rizzo{}.packRat(42).Get().Get().Get()`),
+			want: Info{
+				objectFilter: `jim.Rizzo`,
+				methodFilter: `jim.packRat(int) jim.Gonzo[jim.Gonzo[jim.Gonzo[int]]]`,
+			},
+		},
+		{
+			name: `unexported method resulting in an instance with same type parameter`,
+			obj: parseObject(t, `sidekick`,
+				`package jim
+				type Beaker[T any] struct{}
+				type Honeydew[S any] struct{}
+				func (hd Honeydew[S]) sidekick() Beaker[S] {
+					return Beaker[S]{}
+				}`),
+			want: Info{
+				objectFilter: `jim.Honeydew[any]`,
+				methodFilter: `jim.sidekick() jim.Beaker[any]`,
+			},
+		},
+		{
+			name: `unexported method in interface from named embedded interface`,
+			obj: parseObject(t, `Waldorf`,
+				`package jim
+				type Statler interface{
+					boo()
+				}
+				type Waldorf interface{
+					Statler
+				}`),
+			want: Info{
+				objectFilter: `jim.Waldorf`,
+				deps: map[string]struct{}{
+					`jim.boo()`: {},
+				},
+			},
+		},
+		{
+			name: `unexported method in interface from unnamed embedded interface`,
+			obj: parseObject(t, `Waldorf`,
+				`package jim
+				type Waldorf interface{
+					interface{
+						boo()
+					}
+				}`),
+			want: Info{
+				objectFilter: `jim.Waldorf`,
+				deps: map[string]struct{}{
+					`jim.boo()`: {},
+				},
+			},
+		},
+		{
+			name: `unexported method on instance of generic interface`,
+			obj: parseObject(t, `Waldorf`,
+				`package jim
+				type Statler[T any] interface{
+					boo() T
+				}
+				type Waldorf Statler[string]`),
+			want: Info{
+				objectFilter: `jim.Waldorf`,
+				deps: map[string]struct{}{
+					`jim.boo() string`: {},
+				},
+			},
+		},
+		{
+			name: `struct with self referencing type parameter constraints`,
+			obj: parseObject(t, `Keys`,
+				`package jim
+				func Keys[K comparable, V any, M ~map[K]V](m M) []K {
+					keys := make([]K, 0, len(m))
+					for k := range m {
+						keys = append(keys, k)
+					}
+					return keys
+				}`),
+			want: Info{
+				objectFilter: `jim.Keys[comparable, any, ~map[comparable]any]`,
+			},
+		},
+		{
+			name: `struct with self referencing type parameter constraints`,
+			obj: parseObject(t, `ElectricMayhem`,
+				`package jim
+				type ElectricMayhem[K comparable, V any, M ~map[K]V] interface {
+					keys() []K
+					values() []V
+					asMap() M
+				}`),
+			want: Info{
+				objectFilter: `jim.ElectricMayhem[comparable, any, ~map[comparable]any]`,
+				deps: map[string]struct{}{
+					`jim.keys() []comparable`:         {},
+					`jim.values() []any`:              {},
+					`jim.asMap() ~map[comparable]any`: {},
+				},
+			},
+		},
+		{
+			name: `function with recursive referencing type parameter constraints`,
+			obj: parseObject(t, `doWork`,
+				`package jim
+				type Doozer[T any] interface {
+					comparable
+					Work() T
+				}
+
+				func doWork[T Doozer[T]](a T) T {
+					return a.Work()
+				}`),
+			want: Info{
+				objectFilter: `jim.doWork[jim.Doozer[jim.Doozer[...]]]`,
+			},
+		},
+		{
+			name: `function with recursive referencing multiple type parameter constraints`,
+			obj: parseObject(t, `doWork`,
+				`package jim
+				type Doozer[T, U any] interface {
+					Work() T
+					Play() U
+				}
+
+				func doWork[T Doozer[T, U], U any](a T) T {
+					return a.Work()
+				}`),
+			want: Info{
+				objectFilter: `jim.doWork[jim.Doozer[jim.Doozer[...], any], any]`,
+			},
+		},
+		{
+			name: `function with multiple recursive referencing multiple type parameter constraints`,
+			obj: parseObject(t, `doWork`,
+				`package jim
+				type Doozer[T, U any] interface {
+					Work() T
+					Play() U
+				}
+
+				func doWork[T Doozer[T, U], U Doozer[T, U]](a T) T {
+					return a.Work()
+				}`),
+			want: Info{
+				objectFilter: `jim.doWork[jim.Doozer[jim.Doozer[...], jim.Doozer[...]], jim.Doozer[jim.Doozer[...], jim.Doozer[...]]]`,
+			},
+		},
+		{
+			name: `function with multiple recursive referencing type parameter constraints`,
+			obj: parseObject(t, `doWork`,
+				`package jim
+				type Doozer[T any] interface {
+					Work() T
+				}
+
+				type Fraggle[U any] interface {
+					Play() U
+				}
+
+				func doWork[T Doozer[T], U Fraggle[U]](a T) T {
+					return a.Work()
+				}`),
+			want: Info{
+				objectFilter: `jim.doWork[jim.Doozer[jim.Doozer[...]], jim.Fraggle[jim.Fraggle[...]]]`,
+			},
+		},
+		{
+			name: `function with osculating recursive referencing type parameter constraints`,
+			obj: parseObject(t, `doWork`,
+				`package jim
+				type Doozer[T any] interface {
+					Work() T
+				}
+
+				type Fraggle[U any] interface {
+					Play() U
+				}
+
+				func doWork[T Doozer[U], U Fraggle[T]]() {}`),
+			want: Info{
+				objectFilter: `jim.doWork[jim.Doozer[jim.Fraggle[jim.Doozer[...]]], jim.Fraggle[jim.Doozer[jim.Fraggle[...]]]]`,
+			},
 		},
 	}
 
@@ -219,14 +611,14 @@
 			t.Run(tt.name, func(t *testing.T) {
 				d := &testDecl{}
 				equal(t, d.Dce().unnamed(), true)
-				equal(t, d.Dce().String(), `[unnamed] . -> []`)
+				equal(t, d.Dce().String(), `[unnamed] -> []`)
 				t.Log(`object:`, types.ObjectString(tt.obj, nil))
 
 				d.Dce().SetName(tt.obj)
 				equal(t, d.Dce().unnamed(), tt.want.unnamed())
-				equal(t, d.Dce().importPath, tt.want.importPath)
 				equal(t, d.Dce().objectFilter, tt.want.objectFilter)
 				equal(t, d.Dce().methodFilter, tt.want.methodFilter)
+				equalSlices(t, d.Dce().getDeps(), tt.want.getDeps())
 				equal(t, d.Dce().String(), tt.want.String())
 			})
 		}
@@ -238,11 +630,20 @@
 				d := &testDecl{}
 				t.Log(`object:`, types.ObjectString(tt.obj, nil))
 
-				d.Dce().setDeps(map[types.Object]struct{}{
-					tt.obj: {},
+				wantDeps := []string{}
+				if len(tt.want.objectFilter) > 0 {
+					wantDeps = append(wantDeps, tt.want.objectFilter)
+				}
+				if len(tt.want.methodFilter) > 0 {
+					wantDeps = append(wantDeps, tt.want.methodFilter)
+				}
+				sort.Strings(wantDeps)
+
+				c := Collector{}
+				c.CollectDCEDeps(d, func() {
+					c.DeclareDCEDep(tt.obj)
 				})
-				equal(t, len(d.Dce().deps), 1)
-				equal(t, d.Dce().deps[0], tt.wantDep)
+				equalSlices(t, d.Dce().getDeps(), wantDeps)
 			})
 		}
 	})
@@ -262,6 +663,240 @@
 	errorMatches(t, err, `^may only set the name once for path/to/mogwai\.Gizmo .*$`)
 }
 
+func Test_Info_UsesDeps(t *testing.T) {
+	tests := []struct {
+		name     string
+		id       string // identifier to check for usage and instance
+		line     int    // line number to find the identifier on
+		src      string
+		wantDeps []string
+	}{
+		{
+			name: `usage of specific struct`,
+			id:   `Sinclair`,
+			line: 5,
+			src: `package epsilon3
+				type Sinclair struct{}
+				func (s Sinclair) command() { }
+				func main() {
+					Sinclair{}.command() //<-- line 5
+				}`,
+			wantDeps: []string{`epsilon3.Sinclair`},
+		},
+		{
+			name: `usage of generic struct`,
+			id:   `Sheridan`,
+			line: 5,
+			src: `package epsilon3
+				type Sheridan[T comparable] struct{}
+				func (s Sheridan[T]) command() { }
+				func main() {
+					Sheridan[string]{}.command() //<-- line 5
+				}`,
+			wantDeps: []string{`epsilon3.Sheridan[string]`},
+		},
+		{
+			name: `usage of unexported method of generic struct`,
+			id:   `command`,
+			line: 5,
+			src: `package epsilon3
+				type Sheridan[T comparable] struct{}
+				func (s Sheridan[T]) command() { }
+				func main() {
+					Sheridan[string]{}.command() //<-- line 5
+				}`,
+			// unexported methods need the method filter for matching with
+			// unexported methods on interfaces.
+			wantDeps: []string{
+				`epsilon3.Sheridan[string]`,
+				`epsilon3.command()`,
+			},
+		},
+		{
+			name: `usage of unexported method of generic struct pointer`,
+			id:   `command`,
+			line: 5,
+			src: `package epsilon3
+				type Sheridan[T comparable] struct{}
+				func (s *Sheridan[T]) command() { }
+				func main() {
+					(&Sheridan[string]{}).command() //<-- line 5
+				}`,
+			// unexported methods need the method filter for matching with
+			// unexported methods on interfaces.
+			wantDeps: []string{
+				`epsilon3.Sheridan[string]`,
+				`epsilon3.command()`,
+			},
+		},
+		{
+			name: `invocation of function with implicit type arguments`,
+			id:   `Move`,
+			line: 5,
+			src: `package epsilon3
+				type Ivanova[T any] struct{}
+				func Move[T ~string|~int](i Ivanova[T]) { }
+				func main() {
+					Move(Ivanova[string]{}) //<-- line 5
+				}`,
+			wantDeps: []string{`epsilon3.Move[string]`},
+		},
+		{
+			name: `exported method on a complex generic type`,
+			id:   `Get`,
+			line: 6,
+			src: `package epsilon3
+				type Garibaldi[T any] struct{ v T }
+				func (g Garibaldi[T]) Get() T { return g.v }
+				func main() {
+					michael := Garibaldi[Garibaldi[Garibaldi[int]]]{v: Garibaldi[Garibaldi[int]]{v: Garibaldi[int]{v: 42}}}
+					_ = michael.Get() // <-- line 6
+				}`,
+			wantDeps: []string{`epsilon3.Garibaldi[epsilon3.Garibaldi[epsilon3.Garibaldi[int]]]`},
+		},
+		{
+			name: `unexported method on a complex generic type`,
+			id:   `get`,
+			line: 6,
+			src: `package epsilon3
+				type Garibaldi[T any] struct{ v T }
+				func (g Garibaldi[T]) get() T { return g.v }
+				func main() {
+					michael := Garibaldi[Garibaldi[Garibaldi[int]]]{v: Garibaldi[Garibaldi[int]]{v: Garibaldi[int]{v: 42}}}
+					_ = michael.get() // <-- line 6
+				}`,
+			wantDeps: []string{
+				`epsilon3.Garibaldi[epsilon3.Garibaldi[epsilon3.Garibaldi[int]]]`,
+				`epsilon3.get() epsilon3.Garibaldi[epsilon3.Garibaldi[int]]`,
+			},
+		},
+		{
+			name: `invoke of method with an unnamed interface receiver`,
+			id:   `heal`,
+			line: 8,
+			src: `package epsilon3
+				type Franklin struct{}
+				func (g Franklin) heal() {}
+				func main() {
+					var stephen interface{
+						heal()
+					} = Franklin{}
+					stephen.heal() // <-- line 8
+				}`,
+			wantDeps: []string{
+				`epsilon3.heal()`,
+			},
+		},
+		{
+			name: `invoke a method with a generic return type via instance`,
+			// Based on go/1.19.13/x64/test/dictionaryCapture-noinline.go
+			id:   `lennier`,
+			line: 6,
+			src: `package epsilon3								
+				type delenn[T any] struct { a T }
+				func (d delenn[T]) lennier() T { return d.a }
+				func cocoon() int {
+					x := delenn[int]{a: 7}
+					f := delenn[int].lennier // <-- line 6
+					return f(x)
+				}`,
+			wantDeps: []string{
+				`epsilon3.delenn[int]`,
+				`epsilon3.lennier() int`,
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			d := &testDecl{}
+			uses, inst := parseInstanceUse(t, tt.line, tt.id, tt.src)
+			tArgs := typeListToSlice(inst.TypeArgs)
+			t.Logf(`object: %s with [%s]`, types.ObjectString(uses, nil), (typesutil.TypeList)(tArgs).String())
+
+			c := Collector{}
+			c.CollectDCEDeps(d, func() {
+				c.DeclareDCEDep(uses, tArgs...)
+			})
+			equalSlices(t, d.Dce().getDeps(), tt.wantDeps)
+		})
+	}
+}
+
+func Test_Info_SpecificCasesDeps(t *testing.T) {
+	tests := []struct {
+		name     string
+		obj      types.Object
+		tArgs    []types.Type
+		wantDeps []string
+	}{
+		{
+			name: `struct instantiation with generic object`,
+			obj: parseObject(t, `Mikey`,
+				`package astoria;
+				type Mikey[T comparable] struct{}
+				`),
+			tArgs:    []types.Type{types.Typ[types.String]},
+			wantDeps: []string{`astoria.Mikey[string]`},
+		},
+		{
+			name: `method instantiation with generic object`,
+			obj: parseObject(t, `brand`,
+				`package astoria;
+				type Mikey[T comparable] struct{ a T}
+				func (m Mikey[T]) brand() T {
+					return m.a
+				}`),
+			tArgs: []types.Type{types.Typ[types.String]},
+			wantDeps: []string{
+				`astoria.Mikey[string]`,
+				`astoria.brand() string`,
+			},
+		},
+		{
+			name: `method instantiation with generic object and multiple type parameters`,
+			obj: parseObject(t, `shuffle`,
+				`package astoria;
+				type Chunk[K comparable, V any] struct{ data map[K]V }
+				func (c Chunk[K, V]) shuffle(k K) V {
+					return c.data[k]
+				}`),
+			tArgs: []types.Type{types.Typ[types.String], types.Typ[types.Int]},
+			wantDeps: []string{
+				`astoria.Chunk[string, int]`,
+				`astoria.shuffle(string) int`,
+			},
+		},
+		{
+			name: `method instantiation with generic object renamed type parameters`,
+			obj: parseObject(t, `shuffle`,
+				`package astoria;
+				type Chunk[K comparable, V any] struct{ data map[K]V }
+				func (c Chunk[T, K]) shuffle(k T) K {
+					return c.data[k]
+				}`),
+			tArgs: []types.Type{types.Typ[types.String], types.Typ[types.Int]},
+			wantDeps: []string{
+				`astoria.Chunk[string, int]`,
+				`astoria.shuffle(string) int`,
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			d := &testDecl{}
+			t.Logf(`object: %s with [%s]`, types.ObjectString(tt.obj, nil), (typesutil.TypeList)(tt.tArgs).String())
+
+			c := Collector{}
+			c.CollectDCEDeps(d, func() {
+				c.DeclareDCEDep(tt.obj, tt.tArgs...)
+			})
+			equalSlices(t, d.Dce().getDeps(), tt.wantDeps)
+		})
+	}
+}
+
 func Test_Info_SetAsAlive(t *testing.T) {
 	pkg := testPackage(`fantasia`)
 
@@ -269,11 +904,11 @@
 		obj := quickVar(pkg, `Falkor`)
 		decl := &testDecl{}
 		equal(t, decl.Dce().isAlive(), true) // unnamed is automatically alive
-		equal(t, decl.Dce().String(), `[unnamed] . -> []`)
+		equal(t, decl.Dce().String(), `[unnamed] -> []`)
 
 		decl.Dce().SetAsAlive()
 		equal(t, decl.Dce().isAlive(), true) // still alive but now explicitly alive
-		equal(t, decl.Dce().String(), `[alive] [unnamed] . -> []`)
+		equal(t, decl.Dce().String(), `[alive] [unnamed] -> []`)
 
 		decl.Dce().SetName(obj)
 		equal(t, decl.Dce().isAlive(), true) // alive because SetAsAlive was called
@@ -284,7 +919,7 @@
 		obj := quickVar(pkg, `Artax`)
 		decl := &testDecl{}
 		equal(t, decl.Dce().isAlive(), true) // unnamed is automatically alive
-		equal(t, decl.Dce().String(), `[unnamed] . -> []`)
+		equal(t, decl.Dce().String(), `[unnamed] -> []`)
 
 		decl.Dce().SetName(obj)
 		equal(t, decl.Dce().isAlive(), false) // named so no longer automatically alive
@@ -480,12 +1115,12 @@
 			want: []*testDecl{rincewind, rincewindRun, vimes, vimesRun, vimesRead, vetinari},
 		},
 		{
-			name: `exposed method`,
+			name: `exported method`,
 			deps: []*testDecl{rincewind, rincewindRun},
 			want: []*testDecl{rincewind, rincewindRun, vetinari},
 		},
 		{
-			name: `unexposed method`,
+			name: `unexported method`,
 			deps: []*testDecl{rincewind, rincewindHide},
 			want: []*testDecl{rincewind, rincewindRun, rincewindHide, vetinari},
 		},
@@ -493,6 +1128,7 @@
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
+			vetinari.Dce().deps = nil // reset deps
 			c.CollectDCEDeps(vetinari, func() {
 				for _, decl := range tt.deps {
 					c.DeclareDCEDep(decl.obj)
@@ -540,6 +1176,14 @@
 	return types.NewVar(token.NoPos, pkg, name, types.Typ[types.Int])
 }
 
+func newTypeInfo() *types.Info {
+	return &types.Info{
+		Defs:      map[*ast.Ident]types.Object{},
+		Uses:      map[*ast.Ident]types.Object{},
+		Instances: map[*ast.Ident]types.Instance{},
+	}
+}
+
 func parseObject(t *testing.T, name, source string) types.Object {
 	t.Helper()
 	objects := parseObjects(t, source)
@@ -554,10 +1198,9 @@
 
 func parseObjects(t *testing.T, source string) []types.Object {
 	t.Helper()
-	info := &types.Info{
-		Defs: map[*ast.Ident]types.Object{},
-	}
-	parseInfo(t, source, info)
+	fset := token.NewFileSet()
+	info := newTypeInfo()
+	parsePackage(t, source, fset, info)
 	objects := make([]types.Object, 0, len(info.Defs))
 	for _, obj := range info.Defs {
 		if obj != nil {
@@ -570,9 +1213,22 @@
 	return objects
 }
 
-func parseInfo(t *testing.T, source string, info *types.Info) *types.Package {
+func parseInstanceUse(t *testing.T, lineNo int, idName, source string) (types.Object, types.Instance) {
 	t.Helper()
 	fset := token.NewFileSet()
+	info := newTypeInfo()
+	parsePackage(t, source, fset, info)
+	for id, obj := range info.Uses {
+		if id.Name == idName && fset.Position(id.Pos()).Line == lineNo {
+			return obj, info.Instances[id]
+		}
+	}
+	t.Fatalf(`failed to find %s on line %d`, idName, lineNo)
+	return nil, types.Instance{}
+}
+
+func parsePackage(t *testing.T, source string, fset *token.FileSet, info *types.Info) *types.Package {
+	t.Helper()
 	f, err := parser.ParseFile(fset, `test.go`, source, 0)
 	if err != nil {
 		t.Fatal(`parsing source:`, err)
@@ -626,6 +1282,17 @@
 func equal[T comparable](t *testing.T, got, want T) {
 	t.Helper()
 	if got != want {
-		t.Errorf(`expected %#v but got %#v`, want, got)
+		t.Errorf("Unexpected value was gotten:\t\nexp: %#v\t\ngot: %#v", want, got)
+	}
+}
+
+func equalSlices[T comparable](t *testing.T, got, want []T) {
+	t.Helper()
+	if len(got) != len(want) {
+		t.Errorf("expected %d but got %d\n\texp: %#v\n\tgot: %#v", len(want), len(got), want, got)
+		return
+	}
+	for i, wantElem := range want {
+		equal(t, got[i], wantElem)
 	}
 }
diff --git a/compiler/internal/dce/filters.go b/compiler/internal/dce/filters.go
new file mode 100644
index 0000000..1994ded
--- /dev/null
+++ b/compiler/internal/dce/filters.go
@@ -0,0 +1,340 @@
+package dce
+
+import (
+	"go/types"
+	"sort"
+	"strconv"
+	"strings"
+)
+
+// getFilters determines the DCE filters for the given object.
+// This will return an object filter and optionally return a method filter.
+//
+// Typically, the object filter will always be set and the method filter
+// will be empty unless the object is an unexported method.
+// However, when the object is a method invocation on an unnamed interface type
+// the object filter will be empty and only the method filter will be set.
+// The later shouldn't happen when naming a declaration but only when creating
+// dependencies.
+func getFilters(o types.Object, tArgs []types.Type) (objectFilter, methodFilter string) {
+	if f, ok := o.(*types.Func); ok {
+		sig := f.Type().(*types.Signature)
+		if recv := sig.Recv(); recv != nil {
+			// The object is a method so the object filter is the receiver type
+			// if the receiver type is named, otherwise it's an unnamed interface.
+			typ := recv.Type()
+			if ptrType, ok := typ.(*types.Pointer); ok {
+				typ = ptrType.Elem()
+			}
+			if len(tArgs) <= 0 {
+				tArgs = getTypeArgs(typ)
+			}
+			if named, ok := typ.(*types.Named); ok {
+				objectFilter = getObjectFilter(named.Obj(), tArgs)
+			}
+
+			// The method is not exported so we only need the method filter.
+			if !o.Exported() {
+				methodFilter = getMethodFilter(o, tArgs)
+			}
+			return
+		}
+	}
+
+	// The object is not a method so we only need the object filter.
+	objectFilter = getObjectFilter(o, tArgs)
+	return
+}
+
+// getObjectFilter returns the object filter that functions as the primary
+// name when determining if a declaration is alive or not.
+// See [naming design](./README.md#naming) for more information.
+func getObjectFilter(o types.Object, tArgs []types.Type) string {
+	return (&filterGen{argTypeRemap: tArgs}).Object(o, tArgs)
+}
+
+// getMethodFilter returns the method filter that functions as the secondary
+// name when determining if a declaration is alive or not.
+// See [naming design](./README.md#naming) for more information.
+func getMethodFilter(o types.Object, tArgs []types.Type) string {
+	if sig, ok := o.Type().(*types.Signature); ok {
+		if len(tArgs) <= 0 {
+			if recv := sig.Recv(); recv != nil {
+				tArgs = getTypeArgs(recv.Type())
+			}
+		}
+		gen := &filterGen{argTypeRemap: tArgs}
+		return objectName(o) + gen.Signature(sig)
+	}
+	return ``
+}
+
+// objectName returns the name part of a filter name,
+// including the package path, if available.
+//
+// This is different from `o.Id` since it always includes the package path
+// when available and doesn't add "_." when not available.
+func objectName(o types.Object) string {
+	if o.Pkg() != nil {
+		return o.Pkg().Path() + `.` + o.Name()
+	}
+	return o.Name()
+}
+
+// getTypeArgs gets the type arguments for the given type
+// wether they are instance types or type parameters.
+func getTypeArgs(typ types.Type) []types.Type {
+	switch t := typ.(type) {
+	case *types.Pointer:
+		return getTypeArgs(t.Elem())
+	case *types.Named:
+		if typeArgs := t.TypeArgs(); typeArgs != nil {
+			return typeListToSlice(typeArgs)
+		}
+		if typeParams := t.TypeParams(); typeParams != nil {
+			return typeParamListToSlice(typeParams)
+		}
+	case *types.Signature:
+		if typeParams := t.RecvTypeParams(); typeParams != nil {
+			return typeParamListToSlice(typeParams)
+		}
+		if typeParams := t.TypeParams(); typeParams != nil {
+			return typeParamListToSlice(typeParams)
+		}
+	}
+	return nil
+}
+
+// typeListToSlice returns the list of type arguments for the instance types.
+func typeListToSlice(instTypes *types.TypeList) []types.Type {
+	tArgs := make([]types.Type, instTypes.Len())
+	for i := range tArgs {
+		tArgs[i] = instTypes.At(i)
+	}
+	return tArgs
+}
+
+// typeParamListToSlice returns the list of type arguments for the type parameters.
+func typeParamListToSlice(typeParams *types.TypeParamList) []types.Type {
+	tParams := make([]types.Type, typeParams.Len())
+	for i := range tParams {
+		tParams[i] = typeParams.At(i).Constraint()
+	}
+	return tParams
+}
+
+type processingGroup struct {
+	o     types.Object
+	tArgs []types.Type
+}
+
+func (p processingGroup) is(o types.Object, tArgs []types.Type) bool {
+	if len(p.tArgs) != len(tArgs) || p.o != o {
+		return false
+	}
+	for i, tArg := range tArgs {
+		if p.tArgs[i] != tArg {
+			return false
+		}
+	}
+	return true
+}
+
+type filterGen struct {
+	// argTypeRemap is the instance types in the same order as the
+	// type parameters in the top level object such that the type parameters
+	// index can be used to get the instance type.
+	argTypeRemap []types.Type
+	inProgress   []processingGroup
+}
+
+func (gen *filterGen) startProcessing(o types.Object, tArgs []types.Type) bool {
+	for _, p := range gen.inProgress {
+		if p.is(o, tArgs) {
+			return false
+		}
+	}
+	gen.inProgress = append(gen.inProgress, processingGroup{o, tArgs})
+	return true
+}
+
+func (gen *filterGen) stopProcessing() {
+	gen.inProgress = gen.inProgress[:len(gen.inProgress)-1]
+}
+
+// Object returns an object filter or filter part for an object.
+func (gen *filterGen) Object(o types.Object, tArgs []types.Type) string {
+	filter := objectName(o)
+
+	// Add additional type information for generics and instances.
+	if len(tArgs) <= 0 {
+		tArgs = getTypeArgs(o.Type())
+	}
+	if len(tArgs) > 0 {
+		// Avoid infinite recursion in type arguments by
+		// tracking the current object and type arguments being processed
+		// and skipping if already in progress.
+		if gen.startProcessing(o, tArgs) {
+			filter += gen.TypeArgs(tArgs)
+			gen.stopProcessing()
+		} else {
+			filter += `[...]`
+		}
+	}
+
+	return filter
+}
+
+// Signature returns the filter part containing the signature
+// parameters and results for a function or method, e.g. `(int)(bool,error)`.
+func (gen *filterGen) Signature(sig *types.Signature) string {
+	filter := `(` + gen.Tuple(sig.Params(), sig.Variadic()) + `)`
+	switch sig.Results().Len() {
+	case 0:
+		break
+	case 1:
+		filter += ` ` + gen.Type(sig.Results().At(0).Type())
+	default:
+		filter += `(` + gen.Tuple(sig.Results(), false) + `)`
+	}
+	return filter
+}
+
+// TypeArgs returns the filter part containing the type
+// arguments, e.g. `[any,int|string]`.
+func (gen *filterGen) TypeArgs(tArgs []types.Type) string {
+	parts := make([]string, len(tArgs))
+	for i, tArg := range tArgs {
+		parts[i] = gen.Type(tArg)
+	}
+	return `[` + strings.Join(parts, `, `) + `]`
+}
+
+// Tuple returns the filter part containing parameter or result
+// types for a function, e.g. `(int,string)`, `(int,...string)`.
+func (gen *filterGen) Tuple(t *types.Tuple, variadic bool) string {
+	count := t.Len()
+	parts := make([]string, count)
+	for i := range parts {
+		argType := t.At(i).Type()
+		if i == count-1 && variadic {
+			if slice, ok := argType.(*types.Slice); ok {
+				argType = slice.Elem()
+			}
+			parts[i] = `...` + gen.Type(argType)
+		} else {
+			parts[i] = gen.Type(argType)
+		}
+	}
+	return strings.Join(parts, `, `)
+}
+
+// Type returns the filter part for a single type.
+func (gen *filterGen) Type(typ types.Type) string {
+	switch t := typ.(type) {
+	case types.Object:
+		return gen.Object(t, nil)
+
+	case *types.Array:
+		return `[` + strconv.FormatInt(t.Len(), 10) + `]` + gen.Type(t.Elem())
+	case *types.Chan:
+		return `chan ` + gen.Type(t.Elem())
+	case *types.Interface:
+		return gen.Interface(t)
+	case *types.Map:
+		return `map[` + gen.Type(t.Key()) + `]` + gen.Type(t.Elem())
+	case *types.Named:
+		// Get type args from named instance not generic object
+		return gen.Object(t.Obj(), getTypeArgs(t))
+	case *types.Pointer:
+		return `*` + gen.Type(t.Elem())
+	case *types.Signature:
+		return `func` + gen.Signature(t)
+	case *types.Slice:
+		return `[]` + gen.Type(t.Elem())
+	case *types.Struct:
+		return gen.Struct(t)
+	case *types.TypeParam:
+		return gen.TypeParam(t)
+	default:
+		// Anything else, like basics, just stringify normally.
+		return t.String()
+	}
+}
+
+// Union returns the filter part for a union of types from an type parameter
+// constraint, e.g. `~string|int|~float64`.
+func (gen *filterGen) Union(u *types.Union) string {
+	parts := make([]string, u.Len())
+	for i := range parts {
+		term := u.Term(i)
+		part := gen.Type(term.Type())
+		if term.Tilde() {
+			part = "~" + part
+		}
+		parts[i] = part
+	}
+	// Sort the union so that "string|int" matches "int|string".
+	sort.Strings(parts)
+	return strings.Join(parts, `|`)
+}
+
+// Interface returns the filter part for an interface type or
+// an interface for a type parameter constraint.
+func (gen *filterGen) Interface(inter *types.Interface) string {
+	// Collect all method constraints with method names and signatures.
+	parts := make([]string, inter.NumMethods())
+	for i := range parts {
+		fn := inter.Method(i)
+		parts[i] = fn.Id() + gen.Signature(fn.Type().(*types.Signature))
+	}
+	// Add any union constraints.
+	for i := 0; i < inter.NumEmbeddeds(); i++ {
+		if union, ok := inter.EmbeddedType(i).(*types.Union); ok {
+			parts = append(parts, gen.Union(union))
+		}
+	}
+	// Sort the parts of the interface since the order doesn't matter.
+	// e.g. `interface { a(); b() }` is the same as `interface { b(); a() }`.
+	sort.Strings(parts)
+
+	if len(parts) <= 0 {
+		return `any`
+	}
+	if inter.NumMethods() <= 0 && len(parts) == 1 {
+		return parts[0] // single constraint union, i.e. `bool|~int|string`
+	}
+	return `interface{ ` + strings.Join(parts, `; `) + ` }`
+}
+
+// Struct returns the filter part for a struct type.
+func (gen *filterGen) Struct(s *types.Struct) string {
+	if s.NumFields() <= 0 {
+		return `struct{}`
+	}
+	parts := make([]string, s.NumFields())
+	for i := range parts {
+		f := s.Field(i)
+		// The field name and order is required to be part of the filter since
+		// struct matching rely on field names too. Tags are not needed.
+		// See https://go.dev/ref/spec#Conversions
+		parts[i] = f.Id() + ` ` + gen.Type(f.Type())
+	}
+	return `struct{ ` + strings.Join(parts, `; `) + ` }`
+}
+
+// TypeParam returns the filter part for a type parameter.
+// If there is an argument remap, it will use the remapped type
+// so long as it doesn't map to itself.
+func (gen *filterGen) TypeParam(t *types.TypeParam) string {
+	index := t.Index()
+	if index >= 0 && index < len(gen.argTypeRemap) {
+		if inst := gen.argTypeRemap[index]; inst != t {
+			return gen.Type(inst)
+		}
+	}
+	if t.Constraint() == nil {
+		return `any`
+	}
+	return gen.Type(t.Constraint())
+}
diff --git a/compiler/internal/dce/info.go b/compiler/internal/dce/info.go
index d5993a6..e9a63ba 100644
--- a/compiler/internal/dce/info.go
+++ b/compiler/internal/dce/info.go
@@ -5,8 +5,6 @@
 	"go/types"
 	"sort"
 	"strings"
-
-	"github.com/gopherjs/gopherjs/compiler/typesutil"
 )
 
 // Info contains information used by the dead-code elimination (DCE) logic to
@@ -17,21 +15,22 @@
 	// and will not be eliminated.
 	alive bool
 
-	// importPath is the package path of the package the declaration is in.
-	importPath string
-
-	// Symbol's identifier used by the dead-code elimination logic, not including
-	// package path. If empty, the symbol is assumed to be alive and will not be
-	// eliminated. For methods it is the same as its receiver type identifier.
+	// objectFilter is the primary DCE name for a declaration.
+	// This will be the variable, function, or type identifier.
+	// For methods it is the receiver type identifier.
+	// If empty, the declaration is assumed to be alive.
 	objectFilter string
 
-	// The second part of the identified used by dead-code elimination for methods.
-	// Empty for other types of symbols.
+	// methodFilter is the secondary DCE name for a declaration.
+	// This will be empty if objectFilter is empty.
+	// This will be set to a qualified method name if the objectFilter
+	// can not determine if the declaration is alive on it's own.
+	// See ./README.md for more information.
 	methodFilter string
 
 	// List of fully qualified (including package path) DCE symbol identifiers the
 	// symbol depends on for dead code elimination purposes.
-	deps []string
+	deps map[string]struct{}
 }
 
 // String gets a human-readable representation of the DCE info.
@@ -43,17 +42,20 @@
 	if d.unnamed() {
 		tags += `[unnamed] `
 	}
-	fullName := d.importPath + `.` + d.objectFilter
-	if len(d.methodFilter) > 0 {
-		fullName += `.` + d.methodFilter
+	fullName := ``
+	if len(d.objectFilter) > 0 {
+		fullName += d.objectFilter + ` `
 	}
-	return tags + fullName + ` -> [` + strings.Join(d.deps, `, `) + `]`
+	if len(d.methodFilter) > 0 {
+		fullName += `& ` + d.methodFilter + ` `
+	}
+	return tags + fullName + `-> [` + strings.Join(d.getDeps(), `, `) + `]`
 }
 
 // unnamed returns true if SetName has not been called for this declaration.
 // This indicates that the DCE is not initialized.
 func (d *Info) unnamed() bool {
-	return d.objectFilter == `` && d.methodFilter == ``
+	return d.objectFilter == ``
 }
 
 // isAlive returns true if the declaration is marked as alive.
@@ -74,35 +76,56 @@
 
 // SetName sets the name used by DCE to represent the declaration
 // this DCE info is attached to.
-func (d *Info) SetName(o types.Object) {
+//
+// The given optional type arguments are used to when the object is a
+// function with type parameters or anytime the object doesn't carry them.
+// If not given, this attempts to get the type arguments from the object.
+func (d *Info) SetName(o types.Object, tArgs ...types.Type) {
 	if !d.unnamed() {
 		panic(fmt.Errorf(`may only set the name once for %s`, d.String()))
 	}
 
-	d.importPath = o.Pkg().Path()
-	if typesutil.IsMethod(o) {
-		recv := typesutil.RecvType(o.Type().(*types.Signature)).Obj()
-		d.objectFilter = recv.Name()
-		if !o.Exported() {
-			d.methodFilter = o.Name() + `~`
+	// Determine name(s) for DCE.
+	d.objectFilter, d.methodFilter = getFilters(o, tArgs)
+
+	// Add automatic dependencies for unexported methods on interfaces.
+	if n, ok := o.Type().(*types.Named); ok {
+		if it, ok := n.Underlying().(*types.Interface); ok {
+			for i := it.NumMethods() - 1; i >= 0; i-- {
+				if m := it.Method(i); !m.Exported() {
+					d.addDepName(getMethodFilter(m, tArgs))
+				}
+			}
 		}
-	} else {
-		d.objectFilter = o.Name()
 	}
 }
 
-// setDeps sets the declaration dependencies used by DCE
+// addDep add a declaration dependencies used by DCE
 // for the declaration this DCE info is attached to.
-// This overwrites any prior set dependencies.
-func (d *Info) setDeps(objectSet map[types.Object]struct{}) {
-	deps := make([]string, 0, len(objectSet))
-	for o := range objectSet {
-		qualifiedName := o.Pkg().Path() + "." + o.Name()
-		if typesutil.IsMethod(o) {
-			qualifiedName += "~"
+func (d *Info) addDep(o types.Object, tArgs []types.Type) {
+	objectFilter, methodFilter := getFilters(o, tArgs)
+	d.addDepName(objectFilter)
+	d.addDepName(methodFilter)
+}
+
+// addDepName adds a declaration dependency by name.
+func (d *Info) addDepName(depName string) {
+	if len(depName) > 0 {
+		if d.deps == nil {
+			d.deps = make(map[string]struct{})
 		}
-		deps = append(deps, qualifiedName)
+		d.deps[depName] = struct{}{}
+	}
+}
+
+// getDeps gets the dependencies for the declaration sorted by name.
+func (id *Info) getDeps() []string {
+	deps := make([]string, len(id.deps))
+	i := 0
+	for dep := range id.deps {
+		deps[i] = dep
+		i++
 	}
 	sort.Strings(deps)
-	d.deps = deps
+	return deps
 }
diff --git a/compiler/internal/dce/selector.go b/compiler/internal/dce/selector.go
index 4eea572..3dff490 100644
--- a/compiler/internal/dce/selector.go
+++ b/compiler/internal/dce/selector.go
@@ -42,12 +42,12 @@
 	info := &declInfo[D]{decl: decl}
 
 	if dce.objectFilter != `` {
-		info.objectFilter = dce.importPath + `.` + dce.objectFilter
+		info.objectFilter = dce.objectFilter
 		s.byFilter[info.objectFilter] = append(s.byFilter[info.objectFilter], info)
 	}
 
 	if dce.methodFilter != `` {
-		info.methodFilter = dce.importPath + `.` + dce.methodFilter
+		info.methodFilter = dce.methodFilter
 		s.byFilter[info.methodFilter] = append(s.byFilter[info.methodFilter], info)
 	}
 }
@@ -72,7 +72,7 @@
 
 		// Consider all decls the current one is known to depend on and possible add
 		// them to the live queue.
-		for _, dep := range dce.deps {
+		for _, dep := range dce.getDeps() {
 			if infos, ok := s.byFilter[dep]; ok {
 				delete(s.byFilter, dep)
 				for _, info := range infos {
diff --git a/compiler/internal/typeparams/collect.go b/compiler/internal/typeparams/collect.go
index 0a9ae75..723172d 100644
--- a/compiler/internal/typeparams/collect.go
+++ b/compiler/internal/typeparams/collect.go
@@ -7,7 +7,6 @@
 
 	"github.com/gopherjs/gopherjs/compiler/typesutil"
 	"github.com/gopherjs/gopherjs/internal/govendor/subst"
-	"golang.org/x/exp/typeparams"
 )
 
 // Resolver translates types defined in terms of type parameters into concrete
@@ -141,7 +140,7 @@
 		for i := 0; i < t.NumMethods(); i++ {
 			method := t.Method(i)
 			c.instances.Add(Instance{
-				Object: typeparams.OriginMethod(method), // TODO(nevkontakte): Can be replaced with method.Origin() in Go 1.19.
+				Object: method.Origin(),
 				TArgs:  c.resolver.SubstituteAll(instance.TypeArgs),
 			})
 		}
diff --git a/compiler/internal/typeparams/instance.go b/compiler/internal/typeparams/instance.go
index f847a98..763cd64 100644
--- a/compiler/internal/typeparams/instance.go
+++ b/compiler/internal/typeparams/instance.go
@@ -147,6 +147,18 @@
 	return result
 }
 
+// ForObj returns instances for a given object type belong to. Order is not specified.
+// This returns the same values as `ByObj()[obj]`.
+func (iset *InstanceSet) ForObj(obj types.Object) []Instance {
+	result := []Instance{}
+	for _, inst := range iset.values {
+		if inst.Object == obj {
+			result = append(result, inst)
+		}
+	}
+	return result
+}
+
 // PackageInstanceSets stores an InstanceSet for each package in a program, keyed
 // by import path.
 type PackageInstanceSets map[string]*InstanceSet
diff --git a/compiler/statements.go b/compiler/statements.go
index d4ca764..d8a2026 100644
--- a/compiler/statements.go
+++ b/compiler/statements.go
@@ -443,7 +443,8 @@
 			}
 		case token.TYPE:
 			for _, spec := range decl.Specs {
-				o := fc.pkgCtx.Defs[spec.(*ast.TypeSpec).Name].(*types.TypeName)
+				id := spec.(*ast.TypeSpec).Name
+				o := fc.pkgCtx.Defs[id].(*types.TypeName)
 				fc.pkgCtx.typeNames.Add(o)
 				fc.pkgCtx.DeclareDCEDep(o)
 			}
diff --git a/compiler/utils.go b/compiler/utils.go
index a69d0fe..8fc2da5 100644
--- a/compiler/utils.go
+++ b/compiler/utils.go
@@ -447,7 +447,7 @@
 		return []typeparams.Instance{{Object: o}}
 	}
 
-	return fc.pkgCtx.instanceSet.Pkg(o.Pkg()).ByObj()[o]
+	return fc.pkgCtx.instanceSet.Pkg(o.Pkg()).ForObj(o)
 }
 
 // instName returns a JS expression that refers to the provided instance of a
@@ -458,6 +458,7 @@
 	if inst.IsTrivial() {
 		return objName
 	}
+	fc.pkgCtx.DeclareDCEDep(inst.Object, inst.TArgs...)
 	return fmt.Sprintf("%s[%d /* %v */]", objName, fc.pkgCtx.instanceSet.ID(inst), inst.TArgs)
 }
 
@@ -514,7 +515,7 @@
 	}
 
 	// For anonymous composite types, generate a synthetic package-level type
-	// declaration, which will be reused for all instances of this time. This
+	// declaration, which will be reused for all instances of this type. This
 	// improves performance, since runtime won't have to synthesize the same type
 	// repeatedly.
 	anonType, ok := fc.pkgCtx.anonTypeMap.At(ty).(*types.TypeName)
diff --git a/go.mod b/go.mod
index cfa813b..bf9d40d 100644
--- a/go.mod
+++ b/go.mod
@@ -13,7 +13,6 @@
 	github.com/spf13/cobra v1.2.1
 	github.com/spf13/pflag v1.0.5
 	github.com/visualfc/goembed v0.3.3
-	golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a
 	golang.org/x/sync v0.5.0
 	golang.org/x/sys v0.10.0
 	golang.org/x/term v0.0.0-20220411215600-e5f449aeb171
diff --git a/go.sum b/go.sum
index 65b1d6a..5c6b0d9 100644
--- a/go.sum
+++ b/go.sum
@@ -270,8 +270,6 @@
 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
-golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a h1:8qmSSA8Gz/1kTrCe0nqR0R3Gb/NDhykzWw2q2mWZydM=
-golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=