Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type checking for default events #2812

Merged
merged 18 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion runtime/ast/composite.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ type CompositeLikeDeclaration interface {

const ResourceDestructionDefaultEventName = "ResourceDestroyed"

func IsResourceDestructionDefaultEvent(identifier string) bool {
return identifier == ResourceDestructionDefaultEventName
}

type CompositeDeclaration struct {
Members *Members
DocString string
Expand Down Expand Up @@ -284,7 +288,7 @@ func (d *CompositeDeclaration) ConformanceList() []*NominalType {

func (d *CompositeDeclaration) IsResourceDestructionDefaultEvent() bool {
return d.CompositeKind == common.CompositeKindEvent &&
d.Identifier.Identifier == ResourceDestructionDefaultEventName
IsResourceDestructionDefaultEvent(d.Identifier.Identifier)
}

// FieldDeclarationFlags
Expand Down
2 changes: 1 addition & 1 deletion runtime/parser/declaration.go
Original file line number Diff line number Diff line change
Expand Up @@ -876,7 +876,7 @@ func parseEventDeclaration(
p.next()

// if this is a `ResourceDestroyed` event (i.e., a default event declaration), parse default arguments
parseDefaultArguments := identifier.Identifier == ast.ResourceDestructionDefaultEventName
parseDefaultArguments := ast.IsResourceDestructionDefaultEvent(identifier.Identifier)
parameterList, err := parseParameterList(p, parseDefaultArguments)
if err != nil {
return nil, err
Expand Down
156 changes: 152 additions & 4 deletions runtime/sema/check_composite_declaration.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,13 @@ func (checker *Checker) visitCompositeLikeDeclaration(declaration ast.CompositeL
)
}

if !compositeType.IsResourceType() && compositeType.DefaultDestroyEvent != nil {
checker.report(&DefaultDestroyEventInNonResourceError{
Kind: declaration.DeclarationKind().Name(),
Range: ast.NewRangeFromPositioned(checker.memoryGauge, declaration),
})
}

// NOTE: visit entitlements, then interfaces, then composites
// DON'T use `nestedDeclarations`, because of non-deterministic order

Expand All @@ -283,6 +290,10 @@ func (checker *Checker) visitCompositeLikeDeclaration(declaration ast.CompositeL
}

for _, nestedComposite := range members.Composites() {
if compositeType.DefaultDestroyEvent != nil {
// we enforce elsewhere that each composite can have only one default destroy event
checker.checkDefaultDestroyEvent(compositeType.DefaultDestroyEvent, nestedComposite, compositeType, declaration)
}
dsainati1 marked this conversation as resolved.
Show resolved Hide resolved
ast.AcceptDeclaration[struct{}](nestedComposite, checker)
}

Expand Down Expand Up @@ -428,10 +439,23 @@ func (checker *Checker) declareNestedDeclarations(

firstNestedCompositeDeclaration := nestedCompositeDeclarations[0]

reportInvalidNesting(
firstNestedCompositeDeclaration.DeclarationKind(),
firstNestedCompositeDeclaration.Identifier,
)
// we want to permit this nesting under 2 conditions: the container is a resource declaration,
// and this nested declaration is a default destroy event

if firstNestedCompositeDeclaration.IsResourceDestructionDefaultEvent() {
if len(nestedCompositeDeclarations) > 1 {
firstNestedCompositeDeclaration = nestedCompositeDeclarations[1]
reportInvalidNesting(
firstNestedCompositeDeclaration.DeclarationKind(),
firstNestedCompositeDeclaration.Identifier,
)
}
} else {
reportInvalidNesting(
firstNestedCompositeDeclaration.DeclarationKind(),
firstNestedCompositeDeclaration.Identifier,
)
}

} else if len(nestedInterfaceDeclarations) > 0 {

Expand Down Expand Up @@ -819,6 +843,14 @@ func (checker *Checker) declareCompositeLikeMembersAndValue(
nestedCompositeDeclarationVariable :=
checker.valueActivations.Find(identifier.Identifier)

if ast.IsResourceDestructionDefaultEvent(identifier.Identifier) {
// Find the default event's type declaration
defaultEventType :=
checker.typeActivations.Find(identifier.Identifier)
defaultEventComposite := defaultEventType.Type.(*CompositeType)
compositeType.DefaultDestroyEvent = defaultEventComposite
turbolent marked this conversation as resolved.
Show resolved Hide resolved
}

declarationMembers.Set(
nestedCompositeDeclarationVariable.Identifier,
&Member{
Expand Down Expand Up @@ -1963,6 +1995,122 @@ func (checker *Checker) enumMembersAndOrigins(
return
}

func (checker *Checker) checkDefaultDestroyParamExpressionKind(
arg ast.Expression,
) {
switch arg := arg.(type) {
case *ast.StringExpression,
*ast.BoolExpression,
*ast.NilExpression,
*ast.IntegerExpression,
*ast.FixedPointExpression,
*ast.PathExpression:
break
case *ast.IdentifierExpression:
identifier := arg.Identifier.Identifier
// these are guaranteed to exist at time of destruction, so we allow them
if identifier == SelfIdentifier || identifier == BaseIdentifier {
break
}
// if it's an attachment, then it's also okay
if checker.typeActivations.Find(identifier) != nil {
break
}
checker.report(&DefaultDestroyInvalidArgumentError{
Range: ast.NewRangeFromPositioned(checker.memoryGauge, arg),
})
case *ast.MemberExpression:
checker.checkDefaultDestroyParamExpressionKind(arg.Expression)
case *ast.IndexExpression:
checker.checkDefaultDestroyParamExpressionKind(arg.TargetExpression)
checker.checkDefaultDestroyParamExpressionKind(arg.IndexingExpression)

// indexing expressions on arrays can fail, and must be disallowed, but
// indexing expressions on dicts, or composites (for attachments) will return `nil` and thus never fail
targetExprType := checker.Elaboration.IndexExpressionTypes(arg).IndexedType
// `nil` indicates that the index is a type-index (i.e. for an attachment access)
if targetExprType == nil {
return
}

switch targetExprType := targetExprType.(type) {
case *DictionaryType:
return
case *ReferenceType:
if _, isDictionaryType := targetExprType.Type.(*DictionaryType); isDictionaryType {
return
}
}

checker.report(&DefaultDestroyInvalidArgumentError{
Range: ast.NewRangeFromPositioned(checker.memoryGauge, arg),
})

default:
checker.report(&DefaultDestroyInvalidArgumentError{
Range: ast.NewRangeFromPositioned(checker.memoryGauge, arg),
})
}
}

func (checker *Checker) checkDefaultDestroyEventParam(
param Parameter,
index int,
constructorFunctionParameters []*ast.Parameter,
containerType ContainerType,
containerDeclaration ast.Declaration,
) {
paramType := param.TypeAnnotation.Type
paramExpr := constructorFunctionParameters[index]
dsainati1 marked this conversation as resolved.
Show resolved Hide resolved
paramDefaultArgument := paramExpr.DefaultArgument

// make `self` and `base` available when checking default arguments so the fields of the composite are available
checker.declareSelfValue(containerType, containerDeclaration.DeclarationDocString())
if compositeContainer, isComposite := containerType.(*CompositeType); isComposite && compositeContainer.Kind == common.CompositeKindAttachment {
checker.declareBaseValue(
compositeContainer.baseType,
compositeContainer,
compositeContainer.baseTypeDocString)
}
param.DefaultArgument = checker.VisitExpression(paramDefaultArgument, paramType)

unwrappedParamType := UnwrapOptionalType(paramType)
// default events must have default arguments for all their parameters; this is enforced in the parser
// we want to check that these arguments are all either literals or field accesses, and have primitive types
if !IsSubType(unwrappedParamType, StringType) &&
!IsSubType(unwrappedParamType, NumberType) &&
!IsSubType(unwrappedParamType, TheAddressType) &&
!IsSubType(unwrappedParamType, PathType) &&
!IsSubType(unwrappedParamType, BoolType) {
checker.report(&DefaultDestroyInvalidParameterError{
ParamType: paramType,
Range: ast.NewRangeFromPositioned(checker.memoryGauge, paramExpr),
})
}

checker.checkDefaultDestroyParamExpressionKind(paramDefaultArgument)
}

func (checker *Checker) checkDefaultDestroyEvent(
eventType *CompositeType,
eventDeclaration ast.CompositeLikeDeclaration,
containerType ContainerType,
containerDeclaration ast.Declaration,
) {

// an event definition always has one "constructor" function in its declaration list
members := eventDeclaration.DeclarationMembers()
functions := members.Initializers()
constructorFunctionParameters := functions[0].FunctionDeclaration.ParameterList.Parameters

checker.enterValueScope()
defer checker.leaveValueScope(eventDeclaration.EndPosition, true)

for index, param := range eventType.ConstructorParameters {
checker.checkDefaultDestroyEventParam(param, index, constructorFunctionParameters, containerType, containerDeclaration)
}
}

func (checker *Checker) checkInitializers(
initializers []*ast.SpecialFunctionDeclaration,
fields []*ast.FieldDeclaration,
Expand Down
9 changes: 9 additions & 0 deletions runtime/sema/check_emit_statement.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ func (checker *Checker) VisitEmitStatement(statement *ast.EmitStatement) (_ stru
return
}

if ast.IsResourceDestructionDefaultEvent(compositeType.Identifier) {
checker.report(
&EmitDefaultDestroyEventError{
Range: ast.NewRangeFromPositioned(checker.memoryGauge, statement.InvocationExpression),
},
)
return
}

checker.Elaboration.SetEmitStatementEventType(statement, compositeType)

// Check that the emitted event is declared in the same location
Expand Down
20 changes: 19 additions & 1 deletion runtime/sema/check_interface_declaration.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,13 @@ func (checker *Checker) VisitInterfaceDeclaration(declaration *ast.InterfaceDecl
fieldPositionGetter,
)

if !interfaceType.IsResourceType() && interfaceType.DefaultDestroyEvent != nil {
checker.report(&DefaultDestroyEventInNonResourceError{
Kind: declaration.DeclarationKind().Name(),
Range: ast.NewRangeFromPositioned(checker.memoryGauge, declaration),
})
}

// NOTE: visit entitlements, then interfaces, then composites
// DON'T use `nestedDeclarations`, because of non-deterministic order

Expand All @@ -151,6 +158,9 @@ func (checker *Checker) VisitInterfaceDeclaration(declaration *ast.InterfaceDecl
if nestedComposite.Kind() == common.CompositeKindEvent {
checker.visitCompositeLikeDeclaration(nestedComposite)
}
if interfaceType.DefaultDestroyEvent != nil {
checker.checkDefaultDestroyEvent(interfaceType.DefaultDestroyEvent, nestedComposite, interfaceType, declaration)
}
}

return
Expand Down Expand Up @@ -432,7 +442,15 @@ func (checker *Checker) declareInterfaceMembersAndValue(declaration *ast.Interfa

for _, nestedCompositeDeclaration := range declaration.Members.Composites() {
if nestedCompositeDeclaration.Kind() == common.CompositeKindEvent {
checker.declareNestedEvent(nestedCompositeDeclaration, eventMembers, interfaceType)
if nestedCompositeDeclaration.IsResourceDestructionDefaultEvent() {
// Find the value declaration
nestedEvent :=
checker.typeActivations.Find(nestedCompositeDeclaration.Identifier.Identifier)
defaultEventComposite := nestedEvent.Type.(*CompositeType)
interfaceType.DefaultDestroyEvent = defaultEventComposite
} else {
checker.declareNestedEvent(nestedCompositeDeclaration, eventMembers, interfaceType)
}
}
}
})()
Expand Down
73 changes: 73 additions & 0 deletions runtime/sema/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -2490,6 +2490,23 @@ func (e *EmitNonEventError) Error() string {
)
}

// EmitDefaultDestroyEventError

type EmitDefaultDestroyEventError struct {
ast.Range
}

var _ SemanticError = &EmitDefaultDestroyEventError{}
var _ errors.UserError = &EmitDefaultDestroyEventError{}

func (*EmitDefaultDestroyEventError) isSemanticError() {}

func (*EmitDefaultDestroyEventError) IsUserError() {}

func (e *EmitDefaultDestroyEventError) Error() string {
return "default destruction events may not be explicitly emitted"
}

// EmitImportedEventError

type EmitImportedEventError struct {
Expand Down Expand Up @@ -4657,6 +4674,62 @@ func (e *InvalidAttachmentEntitlementError) EndPosition(common.MemoryGauge) ast.
return e.Pos
}

// DefaultDestroyEventInNonResourceError

type DefaultDestroyEventInNonResourceError struct {
Kind string
ast.Range
}

var _ SemanticError = &DefaultDestroyEventInNonResourceError{}
var _ errors.UserError = &DefaultDestroyEventInNonResourceError{}

func (*DefaultDestroyEventInNonResourceError) isSemanticError() {}

func (*DefaultDestroyEventInNonResourceError) IsUserError() {}

func (e *DefaultDestroyEventInNonResourceError) Error() string {
return fmt.Sprintf(
"cannot declare default destruction event in %s",
e.Kind,
)
}

// DefaultDestroyInvalidArgumentError

type DefaultDestroyInvalidArgumentError struct {
ast.Range
}

var _ SemanticError = &DefaultDestroyInvalidArgumentError{}
var _ errors.UserError = &DefaultDestroyInvalidArgumentError{}

func (*DefaultDestroyInvalidArgumentError) isSemanticError() {}

func (*DefaultDestroyInvalidArgumentError) IsUserError() {}

func (e *DefaultDestroyInvalidArgumentError) Error() string {
return "default destroy event arguments must be literals, member access expressions on `self` or `base`, indexed access expressions on dictionaries, or attachment accesses"
}

// DefaultDestroyInvalidArgumentError

type DefaultDestroyInvalidParameterError struct {
ParamType Type
ast.Range
}

var _ SemanticError = &DefaultDestroyInvalidParameterError{}
var _ errors.UserError = &DefaultDestroyInvalidParameterError{}

func (*DefaultDestroyInvalidParameterError) isSemanticError() {}

func (*DefaultDestroyInvalidParameterError) IsUserError() {}

func (e *DefaultDestroyInvalidParameterError) Error() string {
return fmt.Sprintf("`%s` is not a valid parameter type for a default destroy event", e.ParamType.QualifiedString())
}

// InvalidTypeParameterizedNonNativeFunctionError

type InvalidTypeParameterizedNonNativeFunctionError struct {
Expand Down
11 changes: 8 additions & 3 deletions runtime/sema/type.go
Original file line number Diff line number Diff line change
Expand Up @@ -2954,9 +2954,10 @@ func formatParameter(spaces bool, label, identifier, typeAnnotation string) stri
}

type Parameter struct {
TypeAnnotation TypeAnnotation
Label string
Identifier string
TypeAnnotation TypeAnnotation
DefaultArgument Type
Label string
Identifier string
}

func (p Parameter) String() string {
Expand Down Expand Up @@ -4296,6 +4297,8 @@ type CompositeType struct {
RequiredEntitlements *EntitlementOrderedSet
AttachmentEntitlementAccess *EntitlementMapAccess

DefaultDestroyEvent *CompositeType

cachedIdentifiers *struct {
TypeID TypeID
QualifiedIdentifier string
Expand Down Expand Up @@ -5152,6 +5155,8 @@ type InterfaceType struct {
effectiveInterfaceConformances []Conformance
effectiveInterfaceConformanceSet *InterfaceSet
supportedEntitlements *EntitlementOrderedSet

DefaultDestroyEvent *CompositeType
}

var _ Type = &InterfaceType{}
Expand Down
Loading