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=