From c9474deeb6fb54dfe798e7b9d30923282d99adc2 Mon Sep 17 00:00:00 2001 From: worstell Date: Fri, 3 May 2024 10:39:40 -0400 Subject: [PATCH] feat: add type registry in Go module contexts (#1396) fixes #1386 --- buildengine/build_go_test.go | 71 ++++++++++++++++ buildengine/build_test.go | 11 +++ buildengine/engine_test.go | 2 +- .../testdata/projects/another/another.go | 13 +++ buildengine/testdata/projects/other/other.go | 63 +++++++++++++- .../build-template/_ftl.tmpl/go/main/main.go | 30 +++++-- go-runtime/compile/build.go | 83 ++++++++++++++++++- go-runtime/compile/schema.go | 53 +++++++++--- go-runtime/compile/schema_test.go | 12 +-- go-runtime/ftl/type_registry.go | 50 +++++++++++ 10 files changed, 358 insertions(+), 30 deletions(-) create mode 100644 go-runtime/ftl/type_registry.go diff --git a/buildengine/build_go_test.go b/buildengine/build_go_test.go index 3825823506..f4cd43dbed 100644 --- a/buildengine/build_go_test.go +++ b/buildengine/build_go_test.go @@ -245,3 +245,74 @@ func TestGoModVersion(t *testing.T) { } testBuild(t, bctx, fmt.Sprintf("go version %q is not recent enough for this module, needs minimum version \"9000.1.1\"", runtime.Version()[2:]), []assertion{}) } + +func TestGeneratedTypeRegistry(t *testing.T) { + sch := &schema.Schema{ + Modules: []*schema.Module{ + {Name: "another", Decls: []schema.Decl{ + &schema.Enum{ + Name: "TypeEnum", + Export: true, + Variants: []*schema.EnumVariant{ + {Name: "A", Value: &schema.TypeValue{Value: &schema.Int{}}}, + {Name: "B", Value: &schema.TypeValue{Value: &schema.String{}}}, + }, + }, + }}, + }, + } + expected := `// Code generated by FTL. DO NOT EDIT. +package main + +import ( + "context" + "reflect" + + "github.com/TBD54566975/ftl/common/plugin" + "github.com/TBD54566975/ftl/go-runtime/server" + "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1/ftlv1connect" + "github.com/TBD54566975/ftl/go-runtime/ftl" + + "ftl/other" + "ftl/another" +) + +func main() { + verbConstructor := server.NewUserVerbServer("other", + server.HandleCall(other.Echo), + ) + ctx := context.Background() + typeRegistry := ftl.NewTypeRegistry() + typeRegistry.RegisterSumType(reflect.TypeFor[other.D](), map[string]reflect.Type{ + "Bool": reflect.TypeFor[other.Bool](), + "Bytes": reflect.TypeFor[other.Bytes](), + "Float": reflect.TypeFor[other.Float](), + "Int": reflect.TypeFor[other.Int](), + "U": reflect.TypeFor[other.U](), + "List": reflect.TypeFor[other.List](), + "Map": reflect.TypeFor[other.Map](), + "String": reflect.TypeFor[other.String](), + "Struct": reflect.TypeFor[other.Struct](), + }) + typeRegistry.RegisterSumType(reflect.TypeFor[other.SecondSumType](), map[string]reflect.Type{ + "A": reflect.TypeFor[other.A](), + "B": reflect.TypeFor[other.B](), + }) + typeRegistry.RegisterSumType(reflect.TypeFor[another.TypeEnum](), map[string]reflect.Type{ + "A": reflect.TypeFor[another.A](), + "B": reflect.TypeFor[another.B](), + }) + ctx = context.WithValue(ctx, "typeRegistry", typeRegistry) + + plugin.Start(ctx, "other", verbConstructor, ftlv1connect.VerbServiceName, ftlv1connect.NewVerbServiceHandler) +} +` + bctx := buildContext{ + moduleDir: "testdata/projects/other", + buildDir: "_ftl", + sch: sch, + } + testBuild(t, bctx, "", []assertion{ + assertGeneratedMain(expected), + }) +} diff --git a/buildengine/build_test.go b/buildengine/build_test.go index 757f7d44d6..a417f61365 100644 --- a/buildengine/build_test.go +++ b/buildengine/build_test.go @@ -77,6 +77,17 @@ func assertGeneratedModule(generatedModulePath string, expectedContent string) a } } +func assertGeneratedMain(expectedContent string) assertion { + return func(t testing.TB, bctx buildContext) error { + t.Helper() + output := filepath.Join(bctx.moduleDir, bctx.buildDir, "go/main/main.go") + fileContent, err := os.ReadFile(output) + assert.NoError(t, err) + assert.Equal(t, expectedContent, string(fileContent)) + return nil + } +} + func assertBuildProtoErrors(msgs ...string) assertion { return func(t testing.TB, bctx buildContext) error { t.Helper() diff --git a/buildengine/engine_test.go b/buildengine/engine_test.go index df886fcac5..92c4cad651 100644 --- a/buildengine/engine_test.go +++ b/buildengine/engine_test.go @@ -49,7 +49,7 @@ func TestEngine(t *testing.T) { expected := map[string][]string{ "alpha": {"another", "other", "builtin"}, "another": {"builtin"}, - "other": {"builtin"}, + "other": {"another", "builtin"}, "builtin": {}, } graph, err := engine.Graph() diff --git a/buildengine/testdata/projects/another/another.go b/buildengine/testdata/projects/another/another.go index acb776bc45..b31e16d039 100644 --- a/buildengine/testdata/projects/another/another.go +++ b/buildengine/testdata/projects/another/another.go @@ -7,6 +7,19 @@ import ( "github.com/TBD54566975/ftl/go-runtime/ftl" // Import the FTL SDK. ) +//ftl:enum export +type TypeEnum interface { + tag() +} + +type A int + +func (A) tag() {} + +type B string + +func (B) tag() {} + type EchoRequest struct { Name ftl.Option[string] `json:"name"` } diff --git a/buildengine/testdata/projects/other/other.go b/buildengine/testdata/projects/other/other.go index c15550eca3..235fb3820b 100644 --- a/buildengine/testdata/projects/other/other.go +++ b/buildengine/testdata/projects/other/other.go @@ -5,10 +5,71 @@ import ( "fmt" "github.com/TBD54566975/ftl/go-runtime/ftl" // Import the FTL SDK. + + "ftl/another" ) +//ftl:enum +type D interface { + tag() +} + +type Bool bool + +func (Bool) tag() {} + +type Bytes []byte + +func (Bytes) tag() {} + +type Float float64 + +func (Float) tag() {} + +type Int int + +func (Int) tag() {} + +//type Time time.Time +// +//func (Time) tag() {} + +type U ftl.Unit + +func (U) tag() {} + +type List []string + +func (List) tag() {} + +type Map map[string]string + +func (Map) tag() {} + +type String string + +func (String) tag() {} + +type Struct struct{} + +func (Struct) tag() {} + +//ftl:enum +type SecondSumType interface { + tag2() +} + +type A string + +func (A) tag2() {} + +type B EchoRequest + +func (B) tag2() {} + type EchoRequest struct { - Name ftl.Option[string] `json:"name"` + Name ftl.Option[string] `json:"name"` + ExternalSumType another.TypeEnum } type EchoResponse struct { diff --git a/go-runtime/compile/build-template/_ftl.tmpl/go/main/main.go b/go-runtime/compile/build-template/_ftl.tmpl/go/main/main.go index 020e293176..6072b4eeec 100644 --- a/go-runtime/compile/build-template/_ftl.tmpl/go/main/main.go +++ b/go-runtime/compile/build-template/_ftl.tmpl/go/main/main.go @@ -3,13 +3,17 @@ package main import ( "context" - +{{- if .SumTypes }} + "reflect" +{{ end }} "github.com/TBD54566975/ftl/common/plugin" "github.com/TBD54566975/ftl/go-runtime/server" "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1/ftlv1connect" - -{{- if .Verbs}} - "ftl/{{.Name}}" +{{- if .SumTypes }} + "github.com/TBD54566975/ftl/go-runtime/ftl" +{{- end }} +{{ range mainImports . }} + "ftl/{{.}}" {{- end}} ) @@ -27,5 +31,21 @@ func main() { {{- end}} {{- end}} ) - plugin.Start(context.Background(), "{{.Name}}", verbConstructor, ftlv1connect.VerbServiceName, ftlv1connect.NewVerbServiceHandler) + ctx := context.Background() + + {{- if .SumTypes}} + typeRegistry := ftl.NewTypeRegistry() + {{- end}} + {{- range .SumTypes}} + typeRegistry.RegisterSumType(reflect.TypeFor[{{.Discriminator}}](), map[string]reflect.Type{ + {{- range .Variants}} + "{{.Name}}": reflect.TypeFor[{{.Type}}](), + {{- end}} + }) + {{- end}} + {{- if .SumTypes}} + ctx = context.WithValue(ctx, "typeRegistry", typeRegistry) + {{- end}} + + plugin.Start(ctx, "{{.Name}}", verbConstructor, ftlv1connect.VerbServiceName, ftlv1connect.NewVerbServiceHandler) } diff --git a/go-runtime/compile/build.go b/go-runtime/compile/build.go index 97bb49c8d0..91aeb6bbef 100644 --- a/go-runtime/compile/build.go +++ b/go-runtime/compile/build.go @@ -15,6 +15,8 @@ import ( "unicode" "github.com/TBD54566975/scaffolder" + sets "github.com/deckarep/golang-set/v2" + gomaps "golang.org/x/exp/maps" "golang.org/x/mod/modfile" "golang.org/x/mod/semver" "golang.org/x/sync/errgroup" @@ -50,6 +52,17 @@ type mainModuleContext struct { Name string Verbs []goVerb Replacements []*modfile.Replace + SumTypes []goSumType +} + +type goSumType struct { + Discriminator string + Variants []goSumTypeVariant +} + +type goSumTypeVariant struct { + Name string + Type string } type ModifyFilesTransaction interface { @@ -123,11 +136,13 @@ func Build(ctx context.Context, moduleDir string, sch *schema.Schema, filesTrans buildDir := buildDir(moduleDir) logger.Debugf("Extracting schema") - nativeNames, main, schemaErrs, err := ExtractModuleSchema(moduleDir) + parseResult, err := ExtractModuleSchema(moduleDir) if err != nil { return fmt.Errorf("failed to extract module schema: %w", err) } - if len(schemaErrs) > 0 { + pr := parseResult.MustGet() + main := pr.Module + if schemaErrs := pr.Errors; len(schemaErrs) > 0 { if err := writeSchemaErrors(config, schemaErrs); err != nil { return fmt.Errorf("failed to write errors: %w", err) } @@ -146,7 +161,7 @@ func Build(ctx context.Context, moduleDir string, sch *schema.Schema, filesTrans goVerbs := make([]goVerb, 0, len(main.Decls)) for _, decl := range main.Decls { if verb, ok := decl.(*schema.Verb); ok { - nativeName, ok := nativeNames[verb] + nativeName, ok := pr.NativeNames[verb] if !ok { return fmt.Errorf("missing native name for verb %s", verb.Name) } @@ -167,6 +182,7 @@ func Build(ctx context.Context, moduleDir string, sch *schema.Schema, filesTrans Name: main.Name, Verbs: goVerbs, Replacements: replacements, + SumTypes: getSumTypes(pr.EnumRefs, main, sch, pr.NativeNames), }, scaffolder.Exclude("^go.mod$"), scaffolder.Functions(funcs)); err != nil { return err } @@ -219,7 +235,6 @@ func GenerateStubsForExternalLibrary(ctx context.Context, dir string, schema *sc Schema: schema, Replacements: replacements, }) - } func generateExternalModules(context ExternalModuleContext) error { @@ -314,6 +329,23 @@ var scaffoldFuncs = scaffolder.FuncMap{ } return false }, + "mainImports": func(ctx mainModuleContext) []string { + imports := sets.NewSet[string]() + if len(ctx.Verbs) > 0 { + imports.Add(ctx.Name) + } + for _, st := range ctx.SumTypes { + if i := strings.LastIndex(st.Discriminator, "."); i != -1 { + imports.Add(st.Discriminator[:i]) + } + for _, v := range st.Variants { + if i := strings.LastIndex(v.Type, "."); i != -1 { + imports.Add(v.Type[:i]) + } + } + } + return imports.ToSlice() + }, } func genType(module *schema.Module, t schema.Type) string { @@ -444,3 +476,46 @@ func writeSchemaErrors(config moduleconfig.ModuleConfig, errors []*schema.Error) } return os.WriteFile(filepath.Join(config.AbsDeployDir(), config.Errors), elBytes, 0600) } + +func getSumTypes(enumRefs []*schema.Ref, module *schema.Module, sch *schema.Schema, nativeNames NativeNames) []goSumType { + sumTypes := make(map[string]goSumType) + for _, d := range module.Decls { + if e, ok := d.(*schema.Enum); ok && !e.IsValueEnum() { + variants := make([]goSumTypeVariant, 0, len(e.Variants)) + for _, v := range e.Variants { + variants = append(variants, goSumTypeVariant{ + Name: v.Name, + Type: nativeNames[v], + }) + } + stFqName := nativeNames[d] + sumTypes[stFqName] = goSumType{ + Discriminator: nativeNames[d], + Variants: variants, + } + } + } + + // register sum types from other modules + for _, ref := range enumRefs { + if ref.Module == module.Name { + continue + } + resolved := sch.ResolveRef(ref) + if e, ok := resolved.(*schema.Enum); ok && !e.IsValueEnum() { + variants := make([]goSumTypeVariant, 0, len(e.Variants)) + for _, v := range e.Variants { + variants = append(variants, goSumTypeVariant{ + Name: v.Name, + Type: ref.Module + "." + v.Name, + }) + } + stFqName := ref.Module + "." + e.Name + sumTypes[stFqName] = goSumType{ + Discriminator: stFqName, + Variants: variants, + } + } + } + return gomaps.Values(sumTypes) +} diff --git a/go-runtime/compile/schema.go b/go-runtime/compile/schema.go index 33089ffdc2..f94f623cda 100644 --- a/go-runtime/compile/schema.go +++ b/go-runtime/compile/schema.go @@ -42,10 +42,11 @@ var ( ) // NativeNames is a map of top-level declarations to their native Go names. -type NativeNames map[schema.Decl]string +type NativeNames map[schema.Node]string type enums map[string]*schema.Enum type enumInterfaces map[string]*types.Interface +type enumRefs map[string]*schema.Ref func noEndColumnErrorf(pos token.Pos, format string, args ...interface{}) *schema.Error { return tokenErrorf(pos, "", format, args...) @@ -91,20 +92,31 @@ func (e errorSet) addAll(errs ...*schema.Error) { } } +type ParseResult struct { + Module *schema.Module + NativeNames NativeNames + // EnumRefs contains any external enums referenced by this module. The refs will be resolved and any type enums + // will be registered to the `ftl.TypeRegistry` and provided in the context for this module. + EnumRefs []*schema.Ref + // Errors contains schema validation errors encountered during parsing. + Errors []*schema.Error +} + // ExtractModuleSchema statically parses Go FTL module source into a schema.Module. -func ExtractModuleSchema(dir string) (NativeNames, *schema.Module, []*schema.Error /*schema errors*/, error /*exceptions*/) { +func ExtractModuleSchema(dir string) (optional.Option[ParseResult], error) { pkgs, err := packages.Load(&packages.Config{ Dir: dir, Fset: fset, Mode: packages.NeedName | packages.NeedFiles | packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo, }, "./...") if err != nil { - return nil, nil, nil, err + return optional.None[ParseResult](), err } if len(pkgs) == 0 { - return nil, nil, nil, fmt.Errorf("no packages found in %q, does \"go mod tidy\" need to be run?", dir) + return optional.None[ParseResult](), fmt.Errorf("no packages found in %q, does \"go mod tidy\" need to be run?", dir) } nativeNames := NativeNames{} + eRefs := enumRefs{} // Find module name module := &schema.Module{} merr := []error{} @@ -112,7 +124,7 @@ func ExtractModuleSchema(dir string) (NativeNames, *schema.Module, []*schema.Err for _, pkg := range pkgs { moduleName, ok := ftlModuleFromGoModule(pkg.PkgPath).Get() if !ok { - return nil, nil, nil, fmt.Errorf("package %q is not in the ftl namespace", pkg.PkgPath) + return optional.None[ParseResult](), fmt.Errorf("package %q is not in the ftl namespace", pkg.PkgPath) } module.Name = moduleName if len(pkg.Errors) > 0 { @@ -120,7 +132,8 @@ func ExtractModuleSchema(dir string) (NativeNames, *schema.Module, []*schema.Err merr = append(merr, perr) } } - pctx := &parseContext{pkg: pkg, pkgs: pkgs, module: module, nativeNames: NativeNames{}, enums: enums{}, enumInterfaces: enumInterfaces{}, errors: errorSet{}} + pctx := &parseContext{pkg: pkg, pkgs: pkgs, module: module, nativeNames: NativeNames{}, enums: enums{}, + enumInterfaces: enumInterfaces{}, enumRefs: eRefs, errors: errorSet{}} for _, file := range pkg.Syntax { err := goast.Visit(file, func(node ast.Node, next func() error) (err error) { switch node := node.(type) { @@ -148,7 +161,7 @@ func ExtractModuleSchema(dir string) (NativeNames, *schema.Module, []*schema.Err return next() }) if err != nil { - return nil, nil, nil, err + return optional.None[ParseResult](), err } } for decl, nativeName := range pctx.nativeNames { @@ -163,12 +176,16 @@ func ExtractModuleSchema(dir string) (NativeNames, *schema.Module, []*schema.Err } if len(schemaErrs) > 0 { schema.SortErrorsByPosition(schemaErrs) - return nil, nil, schemaErrs, nil + return optional.Some(ParseResult{Errors: schemaErrs}), nil } if len(merr) > 0 { - return nil, nil, nil, errors.Join(merr...) + return optional.None[ParseResult](), errors.Join(merr...) } - return nativeNames, module, nil, schema.ValidateModule(module) + return optional.Some(ParseResult{ + NativeNames: nativeNames, + Module: module, + EnumRefs: maps.Values(eRefs), + }), schema.ValidateModule(module) } func visitCallExpr(pctx *parseContext, node *ast.CallExpr) { @@ -422,12 +439,12 @@ func visitGenDecl(pctx *parseContext, node *ast.GenDecl) { typ := pctx.pkg.TypesInfo.TypeOf(t.Type) switch typ.Underlying().(type) { case *types.Basic: - if typ, ok := visitType(pctx, node.Pos(), pctx.pkg.TypesInfo.TypeOf(t.Type), isExported).Get(); ok { + if sType, ok := visitType(pctx, node.Pos(), typ, isExported).Get(); ok { enum := &schema.Enum{ Pos: goPosToSchemaPos(node.Pos()), Comments: visitComments(node.Doc), Name: strcase.ToUpperCamel(t.Name.Name), - Type: typ, + Type: sType, Export: isExported, } pctx.enums[t.Name.Name] = enum @@ -444,7 +461,9 @@ func visitGenDecl(pctx *parseContext, node *ast.GenDecl) { Export: isExported, } pctx.enums[t.Name.Name] = enum - pctx.nativeNames[enum] = t.Name.Name + enumType := pctx.pkg.Types.Scope().Lookup(t.Name.Name) + enumName := enumType.Pkg().Name() + "." + enumType.Name() + pctx.nativeNames[enum] = enumName if typ, ok := typ.(*types.Interface); ok { pctx.enumInterfaces[t.Name.Name] = typ } else { @@ -521,6 +540,7 @@ func maybeVisitTypeEnumVariant(pctx *parseContext, node *ast.GenDecl, directives pctx.errors.add(errorf(node, "unsupported type %q for type enum variant", named)) } pctx.enums[enumName].Variants = append(pctx.enums[enumName].Variants, enumVariant) + pctx.nativeNames[enumVariant] = named.Obj().Pkg().Name() + "." + named.Obj().Name() return true } } @@ -872,6 +892,9 @@ func visitType(pctx *parseContext, pos token.Pos, tnode types.Type, isExported b return optional.None[schema.Type]() } enumRef, doneWithVisit := visitEnumType(pctx, pos, named) + if er, ok := enumRef.Get(); ok { + pctx.enumRefs[named.Obj().Name()] = er.(*schema.Ref) //nolint:forcetypeassert + } if doneWithVisit { return enumRef } @@ -941,6 +964,9 @@ func visitType(pctx *parseContext, pos token.Pos, tnode types.Type, isExported b } if named, ok := tnode.(*types.Named); ok { enumRef, doneWithVisit := visitEnumType(pctx, pos, named) + if er, ok := enumRef.Get(); ok { + pctx.enumRefs[named.Obj().Name()] = er.(*schema.Ref) //nolint:forcetypeassert + } if doneWithVisit { return enumRef } @@ -1073,6 +1099,7 @@ type parseContext struct { nativeNames NativeNames enums enums enumInterfaces enumInterfaces + enumRefs enumRefs activeVerb *schema.Verb errors errorSet } diff --git a/go-runtime/compile/schema_test.go b/go-runtime/compile/schema_test.go index 9b5b3645a1..ab8756d844 100644 --- a/go-runtime/compile/schema_test.go +++ b/go-runtime/compile/schema_test.go @@ -43,9 +43,9 @@ func TestExtractModuleSchema(t *testing.T) { } prebuildTestModule(t, "testdata/one", "testdata/two") - _, actual, _, err := ExtractModuleSchema("testdata/one") + r, err := ExtractModuleSchema("testdata/one") assert.NoError(t, err) - actual = schema.Normalise(actual) + actual := schema.Normalise(r.MustGet().Module) expected := `module one { config configValue one.Config secret secretValue String @@ -158,9 +158,9 @@ func TestExtractModuleSchemaTwo(t *testing.T) { if testing.Short() { t.SkipNow() } - _, actual, _, err := ExtractModuleSchema("testdata/two") + r, err := ExtractModuleSchema("testdata/two") assert.NoError(t, err) - actual = schema.Normalise(actual) + actual := schema.Normalise(r.MustGet().Module) expected := `module two { export enum TwoEnum: String { Red = "Red" @@ -285,11 +285,11 @@ func TestErrorReporting(t *testing.T) { t.SkipNow() } pwd, _ := os.Getwd() - _, _, schemaErrs, err := ExtractModuleSchema("testdata/failing") + r, err := ExtractModuleSchema("testdata/failing") assert.NoError(t, err) filename := filepath.Join(pwd, `testdata/failing/failing.go`) - assert.EqualError(t, errors.Join(genericizeErrors(schemaErrs)...), + assert.EqualError(t, errors.Join(genericizeErrors(r.MustGet().Errors)...), filename+":10:13-35: config and secret declarations must have a single string literal argument\n"+ filename+":13:18-52: duplicate config declaration at 12:18-52\n"+ filename+":16:18-52: duplicate secret declaration at 15:18-52\n"+ diff --git a/go-runtime/ftl/type_registry.go b/go-runtime/ftl/type_registry.go new file mode 100644 index 0000000000..35719ac4b5 --- /dev/null +++ b/go-runtime/ftl/type_registry.go @@ -0,0 +1,50 @@ +package ftl + +import ( + "fmt" + "reflect" +) + +// TypeRegistry is a registry of types that can be instantiated by their qualified name. +// It also records sum types and their variants, for use in encoding and decoding. +// +// FTL manages the type registry for you, so you don't need to create one yourself. +type TypeRegistry struct { + // sumTypes associates a sum type discriminator with its variants + sumTypes map[string][]sumTypeVariant + types map[string]reflect.Type +} + +type sumTypeVariant struct { + name string + typeName string +} + +// NewTypeRegistry creates a new type registry. +// The type registry is used to instantiate types by their qualified name at runtime. +func NewTypeRegistry() *TypeRegistry { + return &TypeRegistry{types: map[string]reflect.Type{}, sumTypes: map[string][]sumTypeVariant{}} +} + +// New creates a new instance of the type from the qualified type name. +func (t *TypeRegistry) New(name string) (any, error) { + typ, ok := t.types[name] + if !ok { + return nil, fmt.Errorf("type %q not registered", name) + } + return reflect.New(typ).Interface(), nil +} + +// RegisterSumType registers a Go sum type with the type registry. Sum types are represented as enums in the +// FTL schema. +func (t *TypeRegistry) RegisterSumType(discriminator reflect.Type, variants map[string]reflect.Type) { + dFqName := discriminator.PkgPath() + "." + discriminator.Name() + t.types[dFqName] = discriminator + t.sumTypes[dFqName] = make([]sumTypeVariant, 0, len(variants)) + + for name, v := range variants { + vFqName := v.PkgPath() + "." + v.Name() + t.types[vFqName] = v + t.sumTypes[dFqName] = append(t.sumTypes[dFqName], sumTypeVariant{name: name, typeName: vFqName}) + } +}