Skip to content

Commit 18c10ff

Browse files
committed
Guard generics support behind an experiment flag.
This commit adds support for GOPHERJS_EXPERIMENT environment variable that can be used to enable experimental functionality in the compiler. Experiment flags can be easily added or removed by updating fields of the internal/experiments.Flags struct. For now, only boolean flags are supported, but it should be easy enough to extend if needed. Users need to have GOPHERJS_EXPERIMENT=generics set in their environment to enable support. Adding the flag will allow us to merge generics support into the main branch before it's fully production-ready without creating unnecessary confusion for the users.
1 parent 732a8f6 commit 18c10ff

File tree

7 files changed

+391
-15
lines changed

7 files changed

+391
-15
lines changed

circle.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ orbs:
7474
jobs:
7575
build:
7676
executor: gopherjs
77+
environment:
78+
GOPHERJS_EXPERIMENT: generics
7779
steps:
7880
- setup_and_install_gopherjs
7981
- run:
@@ -129,6 +131,8 @@ jobs:
129131
gopherjs_tests:
130132
executor: gopherjs
131133
parallelism: 4
134+
environment:
135+
GOPHERJS_EXPERIMENT: generics
132136
steps:
133137
- setup_and_install_gopherjs
134138
- run:
@@ -155,6 +159,8 @@ jobs:
155159

156160
gorepo_tests:
157161
executor: gopherjs
162+
environment:
163+
GOPHERJS_EXPERIMENT: generics
158164
parallelism: 4
159165
steps:
160166
- setup_environment
@@ -170,6 +176,8 @@ jobs:
170176
executor:
171177
name: win/default
172178
shell: powershell.exe
179+
environment:
180+
GOPHERJS_EXPERIMENT: generics
173181
steps:
174182
- checkout
175183
- run:
@@ -204,6 +212,8 @@ jobs:
204212
darwin_smoke:
205213
macos:
206214
xcode: 13.4.1 # Mac OS 12.6.1, see https://circleci.com/docs/using-macos/
215+
environment:
216+
GOPHERJS_EXPERIMENT: generics
207217
steps:
208218
- checkout
209219
- setup_environment

compiler/internal/typeparams/utils.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package typeparams
22

3-
import "go/types"
3+
import (
4+
"errors"
5+
"fmt"
6+
"go/types"
7+
)
48

59
// SignatureTypeParams returns receiver type params for methods, or function
610
// type params for standalone functions, or nil for non-generic functions and
@@ -14,3 +18,31 @@ func SignatureTypeParams(sig *types.Signature) *types.TypeParamList {
1418
return nil
1519
}
1620
}
21+
22+
var (
23+
errInstantiatesGenerics = errors.New("instantiates generic type or function")
24+
errDefinesGenerics = errors.New("defines generic type or function")
25+
)
26+
27+
// RequiresGenericsSupport an error if the type-checked code depends on
28+
// generics support.
29+
func RequiresGenericsSupport(info *types.Info) error {
30+
type withTypeParams interface{ TypeParams() *types.TypeParamList }
31+
32+
for ident := range info.Instances {
33+
// Any instantiation means dependency on generics.
34+
return fmt.Errorf("%w: %v", errInstantiatesGenerics, info.ObjectOf(ident))
35+
}
36+
37+
for _, obj := range info.Defs {
38+
if obj == nil {
39+
continue
40+
}
41+
typ, ok := obj.Type().(withTypeParams)
42+
if ok && typ.TypeParams().Len() > 0 {
43+
return fmt.Errorf("%w: %v", errDefinesGenerics, obj)
44+
}
45+
}
46+
47+
return nil
48+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package typeparams
2+
3+
import (
4+
"errors"
5+
"testing"
6+
7+
"github.com/gopherjs/gopherjs/internal/srctesting"
8+
)
9+
10+
func TestRequiresGenericsSupport(t *testing.T) {
11+
t.Run("generic func", func(t *testing.T) {
12+
f := srctesting.New(t)
13+
src := `package foo
14+
func foo[T any](t T) {}`
15+
info, _ := f.Check("pkg/foo", f.Parse("foo.go", src))
16+
17+
err := RequiresGenericsSupport(info)
18+
if !errors.Is(err, errDefinesGenerics) {
19+
t.Errorf("Got: RequiresGenericsSupport() = %v. Want: %v", err, errDefinesGenerics)
20+
}
21+
})
22+
23+
t.Run("generic type", func(t *testing.T) {
24+
f := srctesting.New(t)
25+
src := `package foo
26+
type Foo[T any] struct{t T}`
27+
info, _ := f.Check("pkg/foo", f.Parse("foo.go", src))
28+
29+
err := RequiresGenericsSupport(info)
30+
if !errors.Is(err, errDefinesGenerics) {
31+
t.Errorf("Got: RequiresGenericsSupport() = %v. Want: %v", err, errDefinesGenerics)
32+
}
33+
})
34+
35+
t.Run("imported generic instance", func(t *testing.T) {
36+
f := srctesting.New(t)
37+
f.Info = nil // Do not combine type checking info from different packages.
38+
src1 := `package foo
39+
type Foo[T any] struct{t T}`
40+
f.Check("pkg/foo", f.Parse("foo.go", src1))
41+
42+
src2 := `package bar
43+
import "pkg/foo"
44+
func bar() { _ = foo.Foo[int]{} }`
45+
info, _ := f.Check("pkg/bar", f.Parse("bar.go", src2))
46+
47+
err := RequiresGenericsSupport(info)
48+
if !errors.Is(err, errInstantiatesGenerics) {
49+
t.Errorf("Got: RequiresGenericsSupport() = %v. Want: %v", err, errInstantiatesGenerics)
50+
}
51+
})
52+
53+
t.Run("no generic usage", func(t *testing.T) {
54+
f := srctesting.New(t)
55+
src := `package foo
56+
type Foo struct{}
57+
func foo() { _ = Foo{} }`
58+
info, _ := f.Check("pkg/foo", f.Parse("foo.go", src))
59+
60+
err := RequiresGenericsSupport(info)
61+
if err != nil {
62+
t.Errorf("Got: RequiresGenericsSupport() = %v. Want: nil", err)
63+
}
64+
})
65+
}

compiler/package.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/gopherjs/gopherjs/compiler/internal/symbol"
1818
"github.com/gopherjs/gopherjs/compiler/internal/typeparams"
1919
"github.com/gopherjs/gopherjs/compiler/typesutil"
20+
"github.com/gopherjs/gopherjs/internal/experiments"
2021
"github.com/neelance/astrewrite"
2122
"golang.org/x/tools/go/gcexportdata"
2223
"golang.org/x/tools/go/types/typeutil"
@@ -182,6 +183,9 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor
182183
if err != nil {
183184
return nil, err
184185
}
186+
if genErr := typeparams.RequiresGenericsSupport(typesInfo); genErr != nil && !experiments.Env.Generics {
187+
return nil, fmt.Errorf("package %s requires generics support (https://github.com/gopherjs/gopherjs/issues/1013): %w", importPath, genErr)
188+
}
185189
importContext.Packages[importPath] = typesPkg
186190

187191
exportData := new(bytes.Buffer)

internal/experiments/experiments.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Package experiments managed the list of experimental feature flags supported
2+
// by GopherJS.
3+
//
4+
// GOPHERJS_EXPERIMENT environment variable can be used to control which features
5+
// are enabled.
6+
package experiments
7+
8+
import (
9+
"errors"
10+
"fmt"
11+
"os"
12+
"reflect"
13+
"strconv"
14+
"strings"
15+
)
16+
17+
var (
18+
// ErrInvalidDest is a kind of error returned by parseFlags() when the dest
19+
// argument does not meet the requirements.
20+
ErrInvalidDest = errors.New("invalid flag struct")
21+
// ErrInvalidFormat is a kind of error returned by parseFlags() when the raw
22+
// flag string format is not valid.
23+
ErrInvalidFormat = errors.New("invalid flag string format")
24+
)
25+
26+
var (
27+
// Env contains experiment flag values from the GOPHERJS_EXPERIMENT
28+
// environment variable.
29+
Env Flags
30+
)
31+
32+
func init() {
33+
if err := parseFlags(os.Getenv("GOPHERJS_EXPERIMENT"), &Env); err != nil {
34+
panic(fmt.Errorf("failed to parse GOPHERJS_EXPERIMENT flags: %w", err))
35+
}
36+
}
37+
38+
// Flags contains flags for currently supported experiments.
39+
type Flags struct {
40+
Generics bool `flag:"generics"`
41+
}
42+
43+
// parseFlags parses the `raw` flags string and populates flag values in the
44+
// `dest`.
45+
//
46+
// `raw` is a comma-separated experiment flag list: `<flag1>,<flag2>,...`. Each
47+
// flag may be either `<name>` or `<name>=<value>`. Omitting value is equivalent
48+
// to "<name> = true". Spaces around name and value are trimmed during
49+
// parsing. Flag name can't be empty. If the same flag is specified multiple
50+
// times, the last instance takes effect.
51+
//
52+
// `dest` must be a pointer to a struct, which fields will be populated with
53+
// flag values. Mapping between flag names and fields is established with the
54+
// `flag` field tag. Fields without a flag tag will be left unpopulated.
55+
// If multiple fields are associated with the same flag result is unspecified.
56+
//
57+
// Flags that don't have a corresponding field are silently ignored. This is
58+
// done to avoid fatal errors when an experiment flag is removed from code, but
59+
// remains specified in user's environment.
60+
//
61+
// Currently only boolean flag values are supported, as defined by
62+
// `strconv.ParseBool()`.
63+
func parseFlags(raw string, dest any) error {
64+
ptr := reflect.ValueOf(dest)
65+
if ptr.Type().Kind() != reflect.Pointer || ptr.Type().Elem().Kind() != reflect.Struct {
66+
return fmt.Errorf("%w: must be a pointer to a struct", ErrInvalidDest)
67+
}
68+
if ptr.IsNil() {
69+
return fmt.Errorf("%w: must not be nil", ErrInvalidDest)
70+
}
71+
fields := fieldMap(ptr.Elem())
72+
73+
if raw == "" {
74+
return nil
75+
}
76+
entries := strings.Split(raw, ",")
77+
78+
for _, entry := range entries {
79+
entry = strings.TrimSpace(entry)
80+
var key, val string
81+
if idx := strings.IndexRune(entry, '='); idx != -1 {
82+
key = strings.TrimSpace(entry[0:idx])
83+
val = strings.TrimSpace(entry[idx+1:])
84+
} else {
85+
key = entry
86+
val = "true"
87+
}
88+
89+
if key == "" {
90+
return fmt.Errorf("%w: empty flag name", ErrInvalidFormat)
91+
}
92+
93+
field, ok := fields[key]
94+
if !ok {
95+
// Unknown field value, possibly an obsolete experiment, ignore it.
96+
continue
97+
}
98+
if field.Type().Kind() != reflect.Bool {
99+
return fmt.Errorf("%w: only boolean flags are supported", ErrInvalidDest)
100+
}
101+
b, err := strconv.ParseBool(val)
102+
if err != nil {
103+
return fmt.Errorf("%w: can't parse %q as boolean for flag %q", ErrInvalidFormat, val, key)
104+
}
105+
field.SetBool(b)
106+
}
107+
108+
return nil
109+
}
110+
111+
// fieldMap returns a map of struct fieldMap keyed by the value of the "flag" tag.
112+
//
113+
// `s` must be a struct. Fields without a "flag" tag are ignored. If multiple
114+
// fieldMap have the same flag, the last field wins.
115+
func fieldMap(s reflect.Value) map[string]reflect.Value {
116+
typ := s.Type()
117+
result := map[string]reflect.Value{}
118+
for i := 0; i < typ.NumField(); i++ {
119+
if val, ok := typ.Field(i).Tag.Lookup("flag"); ok {
120+
result[val] = s.Field(i)
121+
}
122+
}
123+
return result
124+
}

0 commit comments

Comments
 (0)