Skip to content

Commit

Permalink
Merge pull request #2812 from onflow/sainati/default-event-checking
Browse files Browse the repository at this point in the history
Type checking for default events
  • Loading branch information
dsainati1 authored Oct 30, 2023
2 parents e63c9e7 + 7931d90 commit 1e79d89
Show file tree
Hide file tree
Showing 8 changed files with 968 additions and 10 deletions.
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
154 changes: 150 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 && nestedComposite.IsResourceDestructionDefaultEvent() {
// we enforce elsewhere that each composite can have only one default destroy event
checker.checkDefaultDestroyEvent(compositeType.DefaultDestroyEvent, nestedComposite, compositeType, declaration)
}
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
}

declarationMembers.Set(
nestedCompositeDeclarationVariable.Identifier,
&Member{
Expand Down Expand Up @@ -1963,6 +1995,120 @@ 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,
astParam *ast.Parameter,
containerType ContainerType,
containerDeclaration ast.Declaration,
) {
paramType := param.TypeAnnotation.Type
paramDefaultArgument := astParam.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, astParam),
})
}

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, constructorFunctionParameters[index], 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

0 comments on commit 1e79d89

Please sign in to comment.