[codegen/hcl2] Improve ConversionFrom perf. (#7545)

- Lazily produce conversion failure diagnostics. This lowers the
  allocation volume and cuts down on execution time by avoiding the
  conversion of source and dest types to strings.
- Add a fast path for union conversions that checks if the source type
  is identical to any of the union's element types. Type equality
  checks are generally much faster than type conversion checks.

These changes lead to a significant speedup in codegen time in
azure-native.
This commit is contained in:
Pat Gavlin 2021-07-16 09:56:26 -07:00 committed by GitHub
parent 490e9cf477
commit 9b23badedd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 74 additions and 53 deletions

View file

@ -35,7 +35,8 @@ func diagf(severity hcl.DiagnosticSeverity, subject hcl.Range, f string, args ..
} }
func ExprNotConvertible(destType Type, expr Expression) *hcl.Diagnostic { func ExprNotConvertible(destType Type, expr Expression) *hcl.Diagnostic {
_, why := destType.conversionFrom(expr.Type(), false, map[Type]struct{}{}) _, whyF := destType.conversionFrom(expr.Type(), false, map[Type]struct{}{})
why := whyF()
if len(why) != 0 { if len(why) != 0 {
return errorf(expr.SyntaxNode().Range(), why[0].Summary) return errorf(expr.SyntaxNode().Range(), why[0].Summary)
} }

View file

@ -19,6 +19,8 @@ import (
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
) )
type lazyDiagnostics func() hcl.Diagnostics
type ConversionKind int type ConversionKind int
const ( const (
@ -42,7 +44,7 @@ type Type interface {
String() string String() string
equals(other Type, seen map[Type]struct{}) bool equals(other Type, seen map[Type]struct{}) bool
conversionFrom(src Type, unifying bool, seen map[Type]struct{}) (ConversionKind, hcl.Diagnostics) conversionFrom(src Type, unifying bool, seen map[Type]struct{}) (ConversionKind, lazyDiagnostics)
string(seen map[Type]struct{}) string string(seen map[Type]struct{}) string
unify(other Type) (Type, ConversionKind) unify(other Type) (Type, ConversionKind)
isType() isType()
@ -74,14 +76,16 @@ func assignableFrom(dest, src Type, assignableFromImpl func() bool) bool {
} }
func conversionFrom(dest, src Type, unifying bool, seen map[Type]struct{}, func conversionFrom(dest, src Type, unifying bool, seen map[Type]struct{},
conversionFromImpl func() (ConversionKind, hcl.Diagnostics)) (ConversionKind, hcl.Diagnostics) { conversionFromImpl func() (ConversionKind, lazyDiagnostics)) (ConversionKind, lazyDiagnostics) {
if dest.Equals(src) || dest == DynamicType { if dest.Equals(src) || dest == DynamicType {
return SafeConversion, nil return SafeConversion, nil
} }
if src, isUnion := src.(*UnionType); isUnion {
switch src := src.(type) {
case *UnionType:
return src.conversionTo(dest, unifying, seen) return src.conversionTo(dest, unifying, seen)
} case *ConstType:
if src, isConst := src.(*ConstType); isConst {
return conversionFrom(dest, src.Type, unifying, seen, conversionFromImpl) return conversionFrom(dest, src.Type, unifying, seen, conversionFromImpl)
} }
if src == DynamicType { if src == DynamicType {

View file

@ -74,8 +74,8 @@ func (t *ConstType) ConversionFrom(src Type) ConversionKind {
return kind return kind
} }
func (t *ConstType) conversionFrom(src Type, unifying bool, seen map[Type]struct{}) (ConversionKind, hcl.Diagnostics) { func (t *ConstType) conversionFrom(src Type, unifying bool, seen map[Type]struct{}) (ConversionKind, lazyDiagnostics) {
return conversionFrom(t, src, unifying, seen, func() (ConversionKind, hcl.Diagnostics) { return conversionFrom(t, src, unifying, seen, func() (ConversionKind, lazyDiagnostics) {
if t.Type.ConversionFrom(src) != NoConversion { if t.Type.ConversionFrom(src) != NoConversion {
return UnsafeConversion, nil return UnsafeConversion, nil
} }

View file

@ -69,7 +69,7 @@ func resolveEventualsImpl(t Type, resolveOutputs bool, seen map[Type]Type) (Type
} }
elementTypes[i] = element elementTypes[i] = element
} }
return NewUnionType(elementTypes...), transform return NewUnionTypeAnnotated(elementTypes, t.Annotations...), transform
case *ObjectType: case *ObjectType:
transform := makeIdentity transform := makeIdentity
if already, ok := seen[t]; ok { if already, ok := seen[t]; ok {
@ -267,10 +267,6 @@ func inputTypeImpl(t Type, seen map[Type]Type) Type {
return t return t
} }
if already, ok := seen[t]; ok {
return already
}
var src Type var src Type
switch t := t.(type) { switch t := t.(type) {
case *OutputType: case *OutputType:
@ -288,6 +284,10 @@ func inputTypeImpl(t Type, seen map[Type]Type) Type {
} }
src = NewUnionTypeAnnotated(elementTypes, t.Annotations...) src = NewUnionTypeAnnotated(elementTypes, t.Annotations...)
case *ObjectType: case *ObjectType:
if already, ok := seen[t]; ok {
return already
}
properties := map[string]Type{} properties := map[string]Type{}
src = NewObjectType(properties, t.Annotations...) src = NewObjectType(properties, t.Annotations...)
seen[t] = src seen[t] = src

View file

@ -92,8 +92,8 @@ func (t *ListType) ConversionFrom(src Type) ConversionKind {
return kind return kind
} }
func (t *ListType) conversionFrom(src Type, unifying bool, seen map[Type]struct{}) (ConversionKind, hcl.Diagnostics) { func (t *ListType) conversionFrom(src Type, unifying bool, seen map[Type]struct{}) (ConversionKind, lazyDiagnostics) {
return conversionFrom(t, src, unifying, seen, func() (ConversionKind, hcl.Diagnostics) { return conversionFrom(t, src, unifying, seen, func() (ConversionKind, lazyDiagnostics) {
switch src := src.(type) { switch src := src.(type) {
case *ListType: case *ListType:
return t.ElementType.conversionFrom(src.ElementType, unifying, seen) return t.ElementType.conversionFrom(src.ElementType, unifying, seen)
@ -101,7 +101,7 @@ func (t *ListType) conversionFrom(src Type, unifying bool, seen map[Type]struct{
return t.ElementType.conversionFrom(src.ElementType, unifying, seen) return t.ElementType.conversionFrom(src.ElementType, unifying, seen)
case *TupleType: case *TupleType:
conversionKind := SafeConversion conversionKind := SafeConversion
var diags hcl.Diagnostics var diags lazyDiagnostics
for _, src := range src.ElementTypes { for _, src := range src.ElementTypes {
if ck, why := t.ElementType.conversionFrom(src, unifying, seen); ck < conversionKind { if ck, why := t.ElementType.conversionFrom(src, unifying, seen); ck < conversionKind {
conversionKind, diags = ck, why conversionKind, diags = ck, why
@ -112,7 +112,7 @@ func (t *ListType) conversionFrom(src Type, unifying bool, seen map[Type]struct{
} }
return conversionKind, diags return conversionKind, diags
} }
return NoConversion, hcl.Diagnostics{typeNotConvertible(t, src)} return NoConversion, func() hcl.Diagnostics { return hcl.Diagnostics{typeNotConvertible(t, src)} }
}) })
} }

View file

@ -93,14 +93,14 @@ func (t *MapType) ConversionFrom(src Type) ConversionKind {
return kind return kind
} }
func (t *MapType) conversionFrom(src Type, unifying bool, seen map[Type]struct{}) (ConversionKind, hcl.Diagnostics) { func (t *MapType) conversionFrom(src Type, unifying bool, seen map[Type]struct{}) (ConversionKind, lazyDiagnostics) {
return conversionFrom(t, src, unifying, seen, func() (ConversionKind, hcl.Diagnostics) { return conversionFrom(t, src, unifying, seen, func() (ConversionKind, lazyDiagnostics) {
switch src := src.(type) { switch src := src.(type) {
case *MapType: case *MapType:
return t.ElementType.conversionFrom(src.ElementType, unifying, seen) return t.ElementType.conversionFrom(src.ElementType, unifying, seen)
case *ObjectType: case *ObjectType:
conversionKind := SafeConversion conversionKind := SafeConversion
var diags hcl.Diagnostics var diags lazyDiagnostics
for _, src := range src.Properties { for _, src := range src.Properties {
if ck, _ := t.ElementType.conversionFrom(src, unifying, seen); ck < conversionKind { if ck, _ := t.ElementType.conversionFrom(src, unifying, seen); ck < conversionKind {
conversionKind = ck conversionKind = ck
@ -111,7 +111,7 @@ func (t *MapType) conversionFrom(src Type, unifying bool, seen map[Type]struct{}
} }
return conversionKind, diags return conversionKind, diags
} }
return NoConversion, hcl.Diagnostics{typeNotConvertible(t, src)} return NoConversion, func() hcl.Diagnostics { return hcl.Diagnostics{typeNotConvertible(t, src)} }
}) })
} }

View file

@ -49,8 +49,8 @@ func (noneType) ConversionFrom(src Type) ConversionKind {
return kind return kind
} }
func (noneType) conversionFrom(src Type, unifying bool, seen map[Type]struct{}) (ConversionKind, hcl.Diagnostics) { func (noneType) conversionFrom(src Type, unifying bool, seen map[Type]struct{}) (ConversionKind, lazyDiagnostics) {
return conversionFrom(NoneType, src, unifying, seen, func() (ConversionKind, hcl.Diagnostics) { return conversionFrom(NoneType, src, unifying, seen, func() (ConversionKind, lazyDiagnostics) {
return NoConversion, nil return NoConversion, nil
}) })
} }

View file

@ -183,13 +183,13 @@ func (t *ObjectType) ConversionFrom(src Type) ConversionKind {
return kind return kind
} }
func (t *ObjectType) conversionFrom(src Type, unifying bool, seen map[Type]struct{}) (ConversionKind, hcl.Diagnostics) { func (t *ObjectType) conversionFrom(src Type, unifying bool, seen map[Type]struct{}) (ConversionKind, lazyDiagnostics) {
return conversionFrom(t, src, unifying, seen, func() (ConversionKind, hcl.Diagnostics) { return conversionFrom(t, src, unifying, seen, func() (ConversionKind, lazyDiagnostics) {
switch src := src.(type) { switch src := src.(type) {
case *ObjectType: case *ObjectType:
if seen != nil { if seen != nil {
if _, ok := seen[t]; ok { if _, ok := seen[t]; ok {
return NoConversion, hcl.Diagnostics{invalidRecursiveType(t)} return NoConversion, func() hcl.Diagnostics { return hcl.Diagnostics{invalidRecursiveType(t)} }
} }
} else { } else {
seen = map[Type]struct{}{} seen = map[Type]struct{}{}
@ -205,7 +205,7 @@ func (t *ObjectType) conversionFrom(src Type, unifying bool, seen map[Type]struc
} }
conversionKind := SafeConversion conversionKind := SafeConversion
var diags hcl.Diagnostics var diags lazyDiagnostics
for k, dst := range t.Properties { for k, dst := range t.Properties {
src, ok := src.Properties[k] src, ok := src.Properties[k]
if !ok { if !ok {
@ -221,7 +221,7 @@ func (t *ObjectType) conversionFrom(src Type, unifying bool, seen map[Type]struc
return conversionKind, diags return conversionKind, diags
case *MapType: case *MapType:
conversionKind := UnsafeConversion conversionKind := UnsafeConversion
var diags hcl.Diagnostics var diags lazyDiagnostics
for _, dst := range t.Properties { for _, dst := range t.Properties {
if ck, why := dst.conversionFrom(src.ElementType, unifying, seen); ck < conversionKind { if ck, why := dst.conversionFrom(src.ElementType, unifying, seen); ck < conversionKind {
conversionKind, diags = ck, why conversionKind, diags = ck, why
@ -232,7 +232,7 @@ func (t *ObjectType) conversionFrom(src Type, unifying bool, seen map[Type]struc
} }
return conversionKind, diags return conversionKind, diags
} }
return NoConversion, hcl.Diagnostics{typeNotConvertible(t, src)} return NoConversion, func() hcl.Diagnostics { return hcl.Diagnostics{typeNotConvertible(t, src)} }
}) })
} }

View file

@ -96,8 +96,8 @@ func (t *OpaqueType) AssignableFrom(src Type) bool {
} }
func (t *OpaqueType) conversionFromImpl( func (t *OpaqueType) conversionFromImpl(
src Type, unifying, checkUnsafe bool, seen map[Type]struct{}) (ConversionKind, hcl.Diagnostics) { src Type, unifying, checkUnsafe bool, seen map[Type]struct{}) (ConversionKind, lazyDiagnostics) {
return conversionFrom(t, src, unifying, seen, func() (ConversionKind, hcl.Diagnostics) { return conversionFrom(t, src, unifying, seen, func() (ConversionKind, lazyDiagnostics) {
if constType, ok := src.(*ConstType); ok { if constType, ok := src.(*ConstType); ok {
return t.conversionFrom(constType.Type, unifying, seen) return t.conversionFrom(constType.Type, unifying, seen)
} }
@ -150,7 +150,7 @@ func (t *OpaqueType) conversionFromImpl(
}) })
} }
func (t *OpaqueType) conversionFrom(src Type, unifying bool, seen map[Type]struct{}) (ConversionKind, hcl.Diagnostics) { func (t *OpaqueType) conversionFrom(src Type, unifying bool, seen map[Type]struct{}) (ConversionKind, lazyDiagnostics) {
return t.conversionFromImpl(src, unifying, true, seen) return t.conversionFromImpl(src, unifying, true, seen)
} }

View file

@ -81,8 +81,8 @@ func (t *OutputType) ConversionFrom(src Type) ConversionKind {
return kind return kind
} }
func (t *OutputType) conversionFrom(src Type, unifying bool, seen map[Type]struct{}) (ConversionKind, hcl.Diagnostics) { func (t *OutputType) conversionFrom(src Type, unifying bool, seen map[Type]struct{}) (ConversionKind, lazyDiagnostics) {
return conversionFrom(t, src, unifying, seen, func() (ConversionKind, hcl.Diagnostics) { return conversionFrom(t, src, unifying, seen, func() (ConversionKind, lazyDiagnostics) {
switch src := src.(type) { switch src := src.(type) {
case *OutputType: case *OutputType:
return t.ElementType.conversionFrom(src.ElementType, unifying, seen) return t.ElementType.conversionFrom(src.ElementType, unifying, seen)

View file

@ -79,8 +79,8 @@ func (t *PromiseType) ConversionFrom(src Type) ConversionKind {
} }
func (t *PromiseType) conversionFrom( func (t *PromiseType) conversionFrom(
src Type, unifying bool, seen map[Type]struct{}) (ConversionKind, hcl.Diagnostics) { src Type, unifying bool, seen map[Type]struct{}) (ConversionKind, lazyDiagnostics) {
return conversionFrom(t, src, unifying, seen, func() (ConversionKind, hcl.Diagnostics) { return conversionFrom(t, src, unifying, seen, func() (ConversionKind, lazyDiagnostics) {
if src, ok := src.(*PromiseType); ok { if src, ok := src.(*PromiseType); ok {
return t.ElementType.conversionFrom(src.ElementType, unifying, seen) return t.ElementType.conversionFrom(src.ElementType, unifying, seen)
} }

View file

@ -76,8 +76,8 @@ func (t *SetType) ConversionFrom(src Type) ConversionKind {
return kind return kind
} }
func (t *SetType) conversionFrom(src Type, unifying bool, seen map[Type]struct{}) (ConversionKind, hcl.Diagnostics) { func (t *SetType) conversionFrom(src Type, unifying bool, seen map[Type]struct{}) (ConversionKind, lazyDiagnostics) {
return conversionFrom(t, src, unifying, seen, func() (ConversionKind, hcl.Diagnostics) { return conversionFrom(t, src, unifying, seen, func() (ConversionKind, lazyDiagnostics) {
switch src := src.(type) { switch src := src.(type) {
case *SetType: case *SetType:
return t.ElementType.conversionFrom(src.ElementType, unifying, seen) return t.ElementType.conversionFrom(src.ElementType, unifying, seen)
@ -94,7 +94,7 @@ func (t *SetType) conversionFrom(src Type, unifying bool, seen map[Type]struct{}
} }
return UnsafeConversion, nil return UnsafeConversion, nil
} }
return NoConversion, hcl.Diagnostics{typeNotConvertible(t, src)} return NoConversion, func() hcl.Diagnostics { return hcl.Diagnostics{typeNotConvertible(t, src)} }
}) })
} }

View file

@ -152,8 +152,8 @@ func (t *TupleType) ConversionFrom(src Type) ConversionKind {
return kind return kind
} }
func (t *TupleType) conversionFrom(src Type, unifying bool, seen map[Type]struct{}) (ConversionKind, hcl.Diagnostics) { func (t *TupleType) conversionFrom(src Type, unifying bool, seen map[Type]struct{}) (ConversionKind, lazyDiagnostics) {
return conversionFrom(t, src, unifying, seen, func() (ConversionKind, hcl.Diagnostics) { return conversionFrom(t, src, unifying, seen, func() (ConversionKind, lazyDiagnostics) {
switch src := src.(type) { switch src := src.(type) {
case *TupleType: case *TupleType:
// When unifying, we will unify two tuples of different length to a new tuple, where elements with matching // When unifying, we will unify two tuples of different length to a new tuple, where elements with matching
@ -166,11 +166,11 @@ func (t *TupleType) conversionFrom(src Type, unifying bool, seen map[Type]struct
} }
if len(t.ElementTypes) != len(src.ElementTypes) { if len(t.ElementTypes) != len(src.ElementTypes) {
return NoConversion, hcl.Diagnostics{tuplesHaveDifferentLengths(t, src)} return NoConversion, func() hcl.Diagnostics { return hcl.Diagnostics{tuplesHaveDifferentLengths(t, src)} }
} }
conversionKind := SafeConversion conversionKind := SafeConversion
var diags hcl.Diagnostics var diags lazyDiagnostics
for i, dst := range t.ElementTypes { for i, dst := range t.ElementTypes {
if ck, why := dst.conversionFrom(src.ElementTypes[i], unifying, seen); ck < conversionKind { if ck, why := dst.conversionFrom(src.ElementTypes[i], unifying, seen); ck < conversionKind {
conversionKind, diags = ck, why conversionKind, diags = ck, why
@ -191,7 +191,7 @@ func (t *TupleType) conversionFrom(src Type, unifying bool, seen map[Type]struct
return conversionKind, diags return conversionKind, diags
case *ListType: case *ListType:
conversionKind := UnsafeConversion conversionKind := UnsafeConversion
var diags hcl.Diagnostics var diags lazyDiagnostics
for _, t := range t.ElementTypes { for _, t := range t.ElementTypes {
if ck, why := t.conversionFrom(src.ElementType, unifying, seen); ck < conversionKind { if ck, why := t.conversionFrom(src.ElementType, unifying, seen); ck < conversionKind {
conversionKind, diags = ck, why conversionKind, diags = ck, why
@ -203,7 +203,7 @@ func (t *TupleType) conversionFrom(src Type, unifying bool, seen map[Type]struct
return conversionKind, diags return conversionKind, diags
case *SetType: case *SetType:
conversionKind := UnsafeConversion conversionKind := UnsafeConversion
var diags hcl.Diagnostics var diags lazyDiagnostics
for _, t := range t.ElementTypes { for _, t := range t.ElementTypes {
if ck, why := t.conversionFrom(src.ElementType, unifying, seen); ck < conversionKind { if ck, why := t.conversionFrom(src.ElementType, unifying, seen); ck < conversionKind {
conversionKind, diags = ck, why conversionKind, diags = ck, why
@ -214,7 +214,7 @@ func (t *TupleType) conversionFrom(src Type, unifying bool, seen map[Type]struct
} }
return conversionKind, diags return conversionKind, diags
} }
return NoConversion, hcl.Diagnostics{typeNotConvertible(t, src)} return NoConversion, func() hcl.Diagnostics { return hcl.Diagnostics{typeNotConvertible(t, src)} }
}) })
} }

View file

@ -173,20 +173,36 @@ func (t *UnionType) ConversionFrom(src Type) ConversionKind {
return kind return kind
} }
func (t *UnionType) conversionFrom(src Type, unifying bool, seen map[Type]struct{}) (ConversionKind, hcl.Diagnostics) { func (t *UnionType) conversionFrom(src Type, unifying bool, seen map[Type]struct{}) (ConversionKind, lazyDiagnostics) {
return conversionFrom(t, src, unifying, seen, func() (ConversionKind, hcl.Diagnostics) { return conversionFrom(t, src, unifying, seen, func() (ConversionKind, lazyDiagnostics) {
var conversionKind ConversionKind var conversionKind ConversionKind
var diags hcl.Diagnostics var diags []lazyDiagnostics
// Fast path: see if the source type is equal to any of the element types. Equality checks are generally
// less expensive that full convertibility checks.
for _, t := range t.ElementTypes {
if src.Equals(t) {
return SafeConversion, nil
}
}
for _, t := range t.ElementTypes { for _, t := range t.ElementTypes {
ck, why := t.conversionFrom(src, unifying, seen) ck, why := t.conversionFrom(src, unifying, seen)
if ck > conversionKind { if ck > conversionKind {
conversionKind = ck conversionKind = ck
} else { } else if why != nil {
diags = diags.Extend(why) diags = append(diags, why)
} }
} }
if conversionKind == NoConversion { if conversionKind == NoConversion {
return NoConversion, diags return NoConversion, func() hcl.Diagnostics {
var all hcl.Diagnostics
for _, why := range diags {
//nolint:errcheck
all.Extend(why())
}
return all
}
} }
return conversionKind, nil return conversionKind, nil
}) })
@ -195,7 +211,7 @@ func (t *UnionType) conversionFrom(src Type, unifying bool, seen map[Type]struct
// If all conversions to a dest type from a union type are safe, the conversion is safe. // If all conversions to a dest type from a union type are safe, the conversion is safe.
// If no conversions to a dest type from a union type exist, the conversion does not exist. // If no conversions to a dest type from a union type exist, the conversion does not exist.
// Otherwise, the conversion is unsafe. // Otherwise, the conversion is unsafe.
func (t *UnionType) conversionTo(dest Type, unifying bool, seen map[Type]struct{}) (ConversionKind, hcl.Diagnostics) { func (t *UnionType) conversionTo(dest Type, unifying bool, seen map[Type]struct{}) (ConversionKind, lazyDiagnostics) {
conversionKind, exists := SafeConversion, false conversionKind, exists := SafeConversion, false
for _, t := range t.ElementTypes { for _, t := range t.ElementTypes {
switch kind, _ := dest.conversionFrom(t, unifying, seen); kind { switch kind, _ := dest.conversionFrom(t, unifying, seen); kind {