From 3295bbbf3ab928918e1eee4ea45bf7ab28932cf6 Mon Sep 17 00:00:00 2001 From: Daniel Sainati Date: Tue, 26 Sep 2023 10:50:43 -0400 Subject: [PATCH 01/16] emit default event --- runtime/interpreter/interpreter.go | 8 +++ runtime/interpreter/interpreter_invocation.go | 24 +++++++- runtime/interpreter/interpreter_statement.go | 31 +++++++---- runtime/interpreter/value.go | 55 ++++++++++++++----- runtime/tests/interpreter/interpreter_test.go | 29 ++++++---- 5 files changed, 108 insertions(+), 39 deletions(-) diff --git a/runtime/interpreter/interpreter.go b/runtime/interpreter/interpreter.go index 74781e7171..3a22d5a066 100644 --- a/runtime/interpreter/interpreter.go +++ b/runtime/interpreter/interpreter.go @@ -1014,6 +1014,8 @@ func (interpreter *Interpreter) declareNonEnumCompositeValue( nestedVariables := map[string]*Variable{} + var destroyEventConstructor FunctionValue + (func() { interpreter.activations.PushNewWithCurrent() defer interpreter.activations.Pop() @@ -1060,6 +1062,11 @@ func (interpreter *Interpreter) declareNonEnumCompositeValue( memberIdentifier := nestedCompositeDeclaration.Identifier.Identifier nestedVariables[memberIdentifier] = nestedVariable + + // statically we know there is at most one of these + if nestedCompositeDeclaration.IsResourceDestructionDefaultEvent() { + destroyEventConstructor = nestedVariable.GetValue().(FunctionValue) + } } for _, nestedAttachmentDeclaration := range members.Attachments() { @@ -1249,6 +1256,7 @@ func (interpreter *Interpreter) declareNonEnumCompositeValue( value.InjectedFields = injectedFields value.Functions = functions + value.defaultDestroyEventConstructor = destroyEventConstructor var self MemberAccessibleValue = value if declaration.Kind() == common.CompositeKindAttachment { diff --git a/runtime/interpreter/interpreter_invocation.go b/runtime/interpreter/interpreter_invocation.go index 8f4fb6dbed..e75df27b5f 100644 --- a/runtime/interpreter/interpreter_invocation.go +++ b/runtime/interpreter/interpreter_invocation.go @@ -185,12 +185,32 @@ func (interpreter *Interpreter) invokeInterpretedFunctionActivated( ) } -// bindParameterArguments binds the argument values to the given parameters +// bindParameterArguments binds the argument values to the given parameters. +// the handling of default arguments makes a number of assumptions to simplify the implementation; +// namely that a) all default arguments are lazily evaluated at the site of the invocation, +// b) that either all the parameters or none of the parameters of a function have default arguments, +// and c) functions cannot currently be explicitly invoked if they have default arguments +// if we plan to generalize this further, we will need to relax those assumptions func (interpreter *Interpreter) bindParameterArguments( parameterList *ast.ParameterList, arguments []Value, ) { - for parameterIndex, parameter := range parameterList.Parameters { + parameters := parameterList.Parameters + + if len(parameters) < 1 { + return + } + + // if the first parameter has a default arg, all of them do, and the arguments list is empty + if parameters[0].DefaultArgument != nil { + // lazily evaluate the default argument expression in this context + for _, parameter := range parameters { + defaultArg := interpreter.evalExpression(parameter.DefaultArgument) + arguments = append(arguments, defaultArg) + } + } + + for parameterIndex, parameter := range parameters { argument := arguments[parameterIndex] interpreter.declareVariable(parameter.Identifier.Identifier, argument) } diff --git a/runtime/interpreter/interpreter_statement.go b/runtime/interpreter/interpreter_statement.go index 59d249c796..bdd86fb6f3 100644 --- a/runtime/interpreter/interpreter_statement.go +++ b/runtime/interpreter/interpreter_statement.go @@ -24,6 +24,7 @@ import ( "github.com/onflow/cadence/runtime/ast" "github.com/onflow/cadence/runtime/common" "github.com/onflow/cadence/runtime/errors" + "github.com/onflow/cadence/runtime/sema" ) func (interpreter *Interpreter) evalStatement(statement ast.Statement) StatementResult { @@ -401,18 +402,7 @@ func (interpreter *Interpreter) visitForStatementBody( return nil, false } -func (interpreter *Interpreter) VisitEmitStatement(statement *ast.EmitStatement) StatementResult { - event, ok := interpreter.evalExpression(statement.InvocationExpression).(*CompositeValue) - if !ok { - panic(errors.NewUnreachableError()) - } - - eventType := interpreter.Program.Elaboration.EmitStatementEventType(statement) - - locationRange := LocationRange{ - Location: interpreter.Location, - HasPosition: statement, - } +func (interpreter *Interpreter) emitEvent(event *CompositeValue, eventType *sema.CompositeType, locationRange LocationRange) { config := interpreter.SharedState.Config @@ -427,6 +417,23 @@ func (interpreter *Interpreter) VisitEmitStatement(statement *ast.EmitStatement) if err != nil { panic(err) } +} + +func (interpreter *Interpreter) VisitEmitStatement(statement *ast.EmitStatement) StatementResult { + + event, ok := interpreter.evalExpression(statement.InvocationExpression).(*CompositeValue) + if !ok { + panic(errors.NewUnreachableError()) + } + + eventType := interpreter.Program.Elaboration.EmitStatementEventType(statement) + + locationRange := LocationRange{ + Location: interpreter.Location, + HasPosition: statement, + } + + interpreter.emitEvent(event, eventType, locationRange) return nil } diff --git a/runtime/interpreter/value.go b/runtime/interpreter/value.go index 434b945b94..fa7e4adc36 100644 --- a/runtime/interpreter/value.go +++ b/runtime/interpreter/value.go @@ -16222,6 +16222,8 @@ type CompositeValue struct { QualifiedIdentifier string Kind common.CompositeKind isDestroyed bool + + defaultDestroyEventConstructor FunctionValue } type ComputedField func(*Interpreter, LocationRange) Value @@ -16457,6 +16459,31 @@ func (v *CompositeValue) Destroy(interpreter *Interpreter, locationRange Locatio }() } + // before actually performing the destruction (i.e. so that any fields are still available), + // emit the default destruction event, if one exists + + if v.defaultDestroyEventConstructor != nil { + var base *EphemeralReferenceValue + var self MemberAccessibleValue = v + if v.Kind == common.CompositeKindAttachment { + base, self = attachmentBaseAndSelfValues(interpreter, v) + } + mockInvocation := NewInvocation( + interpreter, + &self, + base, + nil, + []Value{}, + []sema.Type{}, + nil, + locationRange, + ) + + event := v.defaultDestroyEventConstructor.invoke(mockInvocation).(*CompositeValue) + eventType := interpreter.MustSemaTypeOfValue(event).(*sema.CompositeType) + interpreter.emitEvent(event, eventType, locationRange) + } + storageID := v.StorageID() interpreter.withResourceDestruction( @@ -17273,6 +17300,7 @@ func (v *CompositeValue) Transfer( res.typeID = v.typeID res.staticType = v.staticType res.base = v.base + res.defaultDestroyEventConstructor = v.defaultDestroyEventConstructor } onResourceOwnerChange := config.OnResourceOwnerChange @@ -17345,19 +17373,20 @@ func (v *CompositeValue) Clone(interpreter *Interpreter) Value { } return &CompositeValue{ - dictionary: dictionary, - Location: v.Location, - QualifiedIdentifier: v.QualifiedIdentifier, - Kind: v.Kind, - InjectedFields: v.InjectedFields, - ComputedFields: v.ComputedFields, - NestedVariables: v.NestedVariables, - Functions: v.Functions, - Stringer: v.Stringer, - isDestroyed: v.isDestroyed, - typeID: v.typeID, - staticType: v.staticType, - base: v.base, + dictionary: dictionary, + Location: v.Location, + QualifiedIdentifier: v.QualifiedIdentifier, + Kind: v.Kind, + InjectedFields: v.InjectedFields, + ComputedFields: v.ComputedFields, + NestedVariables: v.NestedVariables, + Functions: v.Functions, + Stringer: v.Stringer, + isDestroyed: v.isDestroyed, + typeID: v.typeID, + staticType: v.staticType, + base: v.base, + defaultDestroyEventConstructor: v.defaultDestroyEventConstructor, } } diff --git a/runtime/tests/interpreter/interpreter_test.go b/runtime/tests/interpreter/interpreter_test.go index b13aa92fc1..a76293f627 100644 --- a/runtime/tests/interpreter/interpreter_test.go +++ b/runtime/tests/interpreter/interpreter_test.go @@ -6794,28 +6794,33 @@ func TestInterpretResourceDestroyExpressionDestructor(t *testing.T) { t.Parallel() - inter := parseCheckAndInterpret(t, ` - var ranDestructor = false + var events []*interpreter.CompositeValue - resource R { } + inter, err := parseCheckAndInterpretWithOptions(t, ` + resource R { + event ResourceDestroyed() + } fun test() { let r <- create R() destroy r } - `) + `, ParseCheckAndInterpretOptions{ + Config: &interpreter.Config{ + OnEventEmitted: func(inter *interpreter.Interpreter, locationRange interpreter.LocationRange, event *interpreter.CompositeValue, eventType *sema.CompositeType) error { + events = append(events, event) + return nil + }, + }, + }) - AssertValuesEqual( - t, - inter, - interpreter.FalseValue, - inter.Globals.Get("ranDestructor").GetValue(), - ) + require.NoError(t, err) - _, err := inter.Invoke("test") + _, err = inter.Invoke("test") require.NoError(t, err) - // DestructorTODO: replace with test for destruction event + require.Len(t, events, 1) + require.Equal(t, events[0].QualifiedIdentifier, "R.ResourceDestroyed") } func TestInterpretResourceDestroyExpressionNestedResources(t *testing.T) { From 59210c1b6083a1afe96cf2e34859848717e08b43 Mon Sep 17 00:00:00 2001 From: Daniel Sainati Date: Tue, 26 Sep 2023 12:30:06 -0400 Subject: [PATCH 02/16] emit events on destroy --- runtime/interpreter/interpreter.go | 28 +- runtime/interpreter/interpreter_invocation.go | 18 -- runtime/interpreter/value.go | 63 +++-- runtime/tests/interpreter/attachments_test.go | 174 ++++++++++--- runtime/tests/interpreter/interpreter_test.go | 245 ++++++++++++------ 5 files changed, 361 insertions(+), 167 deletions(-) diff --git a/runtime/interpreter/interpreter.go b/runtime/interpreter/interpreter.go index 3a22d5a066..1431602436 100644 --- a/runtime/interpreter/interpreter.go +++ b/runtime/interpreter/interpreter.go @@ -1101,6 +1101,29 @@ func (interpreter *Interpreter) declareNonEnumCompositeValue( locationRange := invocation.LocationRange self := *invocation.Self + // the handling of default arguments makes a number of assumptions to simplify the implementation; + // namely that a) all default arguments are lazily evaluated at the site of the invocation, + // b) that either all the parameters or none of the parameters of a function have default arguments, + // and c) functions cannot currently be explicitly invoked if they have default arguments + // if we plan to generalize this further, we will need to relax those assumptions + if len(compositeType.ConstructorParameters) < 1 { + return nil + } + + // event intefaces do not exist + compositeDecl := declaration.(*ast.CompositeDeclaration) + if compositeDecl.IsResourceDestructionDefaultEvent() { + parameters := compositeDecl.DeclarationMembers().Initializers()[0].FunctionDeclaration.ParameterList.Parameters + // if the first parameter has a default arg, all of them do, and the arguments list is empty + if parameters[0].DefaultArgument != nil { + // lazily evaluate the default argument expression in this context + for _, parameter := range parameters { + defaultArg := interpreter.evalExpression(parameter.DefaultArgument) + invocation.Arguments = append(invocation.Arguments, defaultArg) + } + } + } + for i, argument := range invocation.Arguments { parameter := compositeType.ConstructorParameters[i] self.SetMember( @@ -1122,6 +1145,10 @@ func (interpreter *Interpreter) declareNonEnumCompositeValue( functions := interpreter.compositeFunctions(declaration, lexicalScope) + if destroyEventConstructor != nil { + functions[resourceDefaultDestroyEventName] = destroyEventConstructor + } + wrapFunctions := func(code WrapperCode) { // Wrap initializer @@ -1256,7 +1283,6 @@ func (interpreter *Interpreter) declareNonEnumCompositeValue( value.InjectedFields = injectedFields value.Functions = functions - value.defaultDestroyEventConstructor = destroyEventConstructor var self MemberAccessibleValue = value if declaration.Kind() == common.CompositeKindAttachment { diff --git a/runtime/interpreter/interpreter_invocation.go b/runtime/interpreter/interpreter_invocation.go index e75df27b5f..2019df8a52 100644 --- a/runtime/interpreter/interpreter_invocation.go +++ b/runtime/interpreter/interpreter_invocation.go @@ -186,30 +186,12 @@ func (interpreter *Interpreter) invokeInterpretedFunctionActivated( } // bindParameterArguments binds the argument values to the given parameters. -// the handling of default arguments makes a number of assumptions to simplify the implementation; -// namely that a) all default arguments are lazily evaluated at the site of the invocation, -// b) that either all the parameters or none of the parameters of a function have default arguments, -// and c) functions cannot currently be explicitly invoked if they have default arguments -// if we plan to generalize this further, we will need to relax those assumptions func (interpreter *Interpreter) bindParameterArguments( parameterList *ast.ParameterList, arguments []Value, ) { parameters := parameterList.Parameters - if len(parameters) < 1 { - return - } - - // if the first parameter has a default arg, all of them do, and the arguments list is empty - if parameters[0].DefaultArgument != nil { - // lazily evaluate the default argument expression in this context - for _, parameter := range parameters { - defaultArg := interpreter.evalExpression(parameter.DefaultArgument) - arguments = append(arguments, defaultArg) - } - } - for parameterIndex, parameter := range parameters { argument := arguments[parameterIndex] interpreter.declareVariable(parameter.Identifier.Identifier, argument) diff --git a/runtime/interpreter/value.go b/runtime/interpreter/value.go index fa7e4adc36..5fe722669c 100644 --- a/runtime/interpreter/value.go +++ b/runtime/interpreter/value.go @@ -16222,8 +16222,6 @@ type CompositeValue struct { QualifiedIdentifier string Kind common.CompositeKind isDestroyed bool - - defaultDestroyEventConstructor FunctionValue } type ComputedField func(*Interpreter, LocationRange) Value @@ -16233,7 +16231,8 @@ type CompositeField struct { Name string } -const attachmentNamePrefix = "$" +const unrepresentableNamePrefix = "$" +const resourceDefaultDestroyEventName = unrepresentableNamePrefix + "ResourceDestroyed" var _ TypeIndexableValue = &CompositeValue{} @@ -16429,6 +16428,10 @@ func (v *CompositeValue) IsDestroyed() bool { return v.isDestroyed } +func (v *CompositeValue) defaultDestroyEventConstructor() FunctionValue { + return v.Functions[resourceDefaultDestroyEventName] +} + func (v *CompositeValue) Destroy(interpreter *Interpreter, locationRange LocationRange) { interpreter.ReportComputation(common.ComputationKindDestroyCompositeValue, 1) @@ -16460,18 +16463,26 @@ func (v *CompositeValue) Destroy(interpreter *Interpreter, locationRange Locatio } // before actually performing the destruction (i.e. so that any fields are still available), - // emit the default destruction event, if one exists + // compute the default arguments of the default destruction event (if it exists). However, + // wait until after the destruction completes to actually emit the event, so that the correct order + // is preserved and nested resource destroy events happen first - if v.defaultDestroyEventConstructor != nil { - var base *EphemeralReferenceValue + // the default destroy event constructor is encoded as a function on the resource (with an unrepresentable name) + // so that we can leverage existing atree encoding and decoding. However, we need to make sure functions are initialized + // if the composite was recently loaded from storage + v.InitializeFunctions(interpreter) + if constructor := v.defaultDestroyEventConstructor(); constructor != nil { var self MemberAccessibleValue = v if v.Kind == common.CompositeKindAttachment { + var base *EphemeralReferenceValue base, self = attachmentBaseAndSelfValues(interpreter, v) + interpreter.declareVariable(sema.BaseIdentifier, base) } + interpreter.declareVariable(sema.SelfIdentifier, self) mockInvocation := NewInvocation( interpreter, - &self, - base, + nil, + nil, nil, []Value{}, []sema.Type{}, @@ -16479,9 +16490,9 @@ func (v *CompositeValue) Destroy(interpreter *Interpreter, locationRange Locatio locationRange, ) - event := v.defaultDestroyEventConstructor.invoke(mockInvocation).(*CompositeValue) + event := constructor.invoke(mockInvocation).(*CompositeValue) eventType := interpreter.MustSemaTypeOfValue(event).(*sema.CompositeType) - interpreter.emitEvent(event, eventType, locationRange) + defer interpreter.emitEvent(event, eventType, locationRange) } storageID := v.StorageID() @@ -17300,7 +17311,6 @@ func (v *CompositeValue) Transfer( res.typeID = v.typeID res.staticType = v.staticType res.base = v.base - res.defaultDestroyEventConstructor = v.defaultDestroyEventConstructor } onResourceOwnerChange := config.OnResourceOwnerChange @@ -17373,20 +17383,19 @@ func (v *CompositeValue) Clone(interpreter *Interpreter) Value { } return &CompositeValue{ - dictionary: dictionary, - Location: v.Location, - QualifiedIdentifier: v.QualifiedIdentifier, - Kind: v.Kind, - InjectedFields: v.InjectedFields, - ComputedFields: v.ComputedFields, - NestedVariables: v.NestedVariables, - Functions: v.Functions, - Stringer: v.Stringer, - isDestroyed: v.isDestroyed, - typeID: v.typeID, - staticType: v.staticType, - base: v.base, - defaultDestroyEventConstructor: v.defaultDestroyEventConstructor, + dictionary: dictionary, + Location: v.Location, + QualifiedIdentifier: v.QualifiedIdentifier, + Kind: v.Kind, + InjectedFields: v.InjectedFields, + ComputedFields: v.ComputedFields, + NestedVariables: v.NestedVariables, + Functions: v.Functions, + Stringer: v.Stringer, + isDestroyed: v.isDestroyed, + typeID: v.typeID, + staticType: v.staticType, + base: v.base, } } @@ -17565,7 +17574,7 @@ func (v *CompositeValue) setBaseValue(interpreter *Interpreter, base *CompositeV } func attachmentMemberName(ty sema.Type) string { - return attachmentNamePrefix + string(ty.ID()) + return unrepresentableNamePrefix + string(ty.ID()) } func (v *CompositeValue) getAttachmentValue(interpreter *Interpreter, locationRange LocationRange, ty sema.Type) *CompositeValue { @@ -17705,7 +17714,7 @@ func (v *CompositeValue) forEachAttachment(interpreter *Interpreter, _ LocationR if key == nil { break } - if strings.HasPrefix(string(key.(StringAtreeValue)), attachmentNamePrefix) { + if strings.HasPrefix(string(key.(StringAtreeValue)), unrepresentableNamePrefix) { attachment, ok := MustConvertStoredValue(interpreter, value).(*CompositeValue) if !ok { panic(errors.NewExternalError(err)) diff --git a/runtime/tests/interpreter/attachments_test.go b/runtime/tests/interpreter/attachments_test.go index 52fdddeba6..a6894e5a75 100644 --- a/runtime/tests/interpreter/attachments_test.go +++ b/runtime/tests/interpreter/attachments_test.go @@ -1239,71 +1239,139 @@ func TestInterpretAttachmentDestructor(t *testing.T) { t.Parallel() - inter := parseCheckAndInterpret(t, ` - var destructorRun = false - resource R {} - attachment A for R {} + var events []*interpreter.CompositeValue + + inter, err := parseCheckAndInterpretWithOptions(t, ` + resource R { + event ResourceDestroyed() + } + attachment A for R { + event ResourceDestroyed() + } fun test() { let r <- create R() let r2 <- attach A() to <-r destroy r2 } - `) + `, ParseCheckAndInterpretOptions{ + Config: &interpreter.Config{ + OnEventEmitted: func(inter *interpreter.Interpreter, locationRange interpreter.LocationRange, event *interpreter.CompositeValue, eventType *sema.CompositeType) error { + events = append(events, event) + return nil + }, + }, + CheckerConfig: &sema.Config{ + AttachmentsEnabled: true, + }, + }) + require.NoError(t, err) - _, err := inter.Invoke("test") + _, err = inter.Invoke("test") require.NoError(t, err) - // DestructorTODO: replace with test for destruction event of A + require.Len(t, events, 2) + require.Equal(t, events[0].QualifiedIdentifier, "A.ResourceDestroyed") + require.Equal(t, events[1].QualifiedIdentifier, "R.ResourceDestroyed") }) t.Run("base destructor executed last", func(t *testing.T) { t.Parallel() - inter := parseCheckAndInterpret(t, ` - resource R {} - attachment A for R { } - attachment B for R { } - attachment C for R { } + var events []*interpreter.CompositeValue + + inter, err := parseCheckAndInterpretWithOptions(t, ` + resource R { + event ResourceDestroyed() + } + attachment A for R { + event ResourceDestroyed() + } + attachment B for R { + event ResourceDestroyed() + } + attachment C for R { + event ResourceDestroyed() + } fun test() { let r <- create R() let r2 <- attach A() to <- attach B() to <- attach C() to <-r destroy r2 } - `) + `, ParseCheckAndInterpretOptions{ + Config: &interpreter.Config{ + OnEventEmitted: func(inter *interpreter.Interpreter, locationRange interpreter.LocationRange, event *interpreter.CompositeValue, eventType *sema.CompositeType) error { + events = append(events, event) + return nil + }, + }, + CheckerConfig: &sema.Config{ + AttachmentsEnabled: true, + }, + }) + require.NoError(t, err) - _, err := inter.Invoke("test") + _, err = inter.Invoke("test") require.NoError(t, err) - // DestructorTODO: replace with test for destruction event for R being the last emitted + require.Len(t, events, 4) + // the only part of this order that is important is that `R` is last + require.Equal(t, events[0].QualifiedIdentifier, "B.ResourceDestroyed") + require.Equal(t, events[1].QualifiedIdentifier, "C.ResourceDestroyed") + require.Equal(t, events[2].QualifiedIdentifier, "A.ResourceDestroyed") + require.Equal(t, events[3].QualifiedIdentifier, "R.ResourceDestroyed") }) t.Run("remove runs destroy", func(t *testing.T) { - inter := parseCheckAndInterpret(t, ` - var destructorRun = false - resource R {} - attachment A for R {} + var events []*interpreter.CompositeValue + + inter, err := parseCheckAndInterpretWithOptions(t, ` + resource R { + event ResourceDestroyed() + } + attachment A for R { + event ResourceDestroyed() + } fun test(): @R { let r <- create R() let r2 <- attach A() to <-r remove A from r2 return <-r2 } - `) + `, ParseCheckAndInterpretOptions{ + Config: &interpreter.Config{ + OnEventEmitted: func(inter *interpreter.Interpreter, locationRange interpreter.LocationRange, event *interpreter.CompositeValue, eventType *sema.CompositeType) error { + events = append(events, event) + return nil + }, + }, + CheckerConfig: &sema.Config{ + AttachmentsEnabled: true, + }, + }) + require.NoError(t, err) - _, err := inter.Invoke("test") + _, err = inter.Invoke("test") require.NoError(t, err) - // DestructorTODO: replace with test for destruction event of both A + require.Len(t, events, 1) + require.Equal(t, events[0].QualifiedIdentifier, "A.ResourceDestroyed") }) t.Run("remove runs resource field destroy", func(t *testing.T) { - inter := parseCheckAndInterpret(t, ` - resource R {} - resource R2 {} + var events []*interpreter.CompositeValue + + inter, err := parseCheckAndInterpretWithOptions(t, ` + resource R { + event ResourceDestroyed() + } + resource R2 { + event ResourceDestroyed() + } attachment A for R { + event ResourceDestroyed() let r2: @R2 init() { self.r2 <- create R2() @@ -1315,21 +1383,43 @@ func TestInterpretAttachmentDestructor(t *testing.T) { remove A from r2 return <-r2 } - `) + `, ParseCheckAndInterpretOptions{ + Config: &interpreter.Config{ + OnEventEmitted: func(inter *interpreter.Interpreter, locationRange interpreter.LocationRange, event *interpreter.CompositeValue, eventType *sema.CompositeType) error { + events = append(events, event) + return nil + }, + }, + CheckerConfig: &sema.Config{ + AttachmentsEnabled: true, + }, + }) + require.NoError(t, err) - _, err := inter.Invoke("test") + _, err = inter.Invoke("test") require.NoError(t, err) - // DestructorTODO: replace with test for destruction event of R2 + require.Len(t, events, 2) + require.Equal(t, events[0].QualifiedIdentifier, "R2.ResourceDestroyed") + require.Equal(t, events[1].QualifiedIdentifier, "A.ResourceDestroyed") }) t.Run("nested attachments destroyed", func(t *testing.T) { - inter := parseCheckAndInterpret(t, ` - resource R {} - resource R2 {} - attachment B for R2 { } + var events []*interpreter.CompositeValue + + inter, err := parseCheckAndInterpretWithOptions(t, ` + resource R { + event ResourceDestroyed() + } + resource R2 { + event ResourceDestroyed() + } + attachment B for R2 { + event ResourceDestroyed() + } attachment A for R { + event ResourceDestroyed() let r2: @R2 init() { self.r2 <- attach B() to <-create R2() @@ -1341,12 +1431,26 @@ func TestInterpretAttachmentDestructor(t *testing.T) { remove A from r2 return <-r2 } - `) + `, ParseCheckAndInterpretOptions{ + Config: &interpreter.Config{ + OnEventEmitted: func(inter *interpreter.Interpreter, locationRange interpreter.LocationRange, event *interpreter.CompositeValue, eventType *sema.CompositeType) error { + events = append(events, event) + return nil + }, + }, + CheckerConfig: &sema.Config{ + AttachmentsEnabled: true, + }, + }) + require.NoError(t, err) - _, err := inter.Invoke("test") + _, err = inter.Invoke("test") require.NoError(t, err) - // DestructorTODO: replace with test for destruction event of both B + require.Len(t, events, 3) + require.Equal(t, events[0].QualifiedIdentifier, "B.ResourceDestroyed") + require.Equal(t, events[1].QualifiedIdentifier, "R2.ResourceDestroyed") + require.Equal(t, events[2].QualifiedIdentifier, "A.ResourceDestroyed") }) } diff --git a/runtime/tests/interpreter/interpreter_test.go b/runtime/tests/interpreter/interpreter_test.go index a76293f627..77bf6f23b0 100644 --- a/runtime/tests/interpreter/interpreter_test.go +++ b/runtime/tests/interpreter/interpreter_test.go @@ -6486,10 +6486,11 @@ func TestInterpretResourceMoveInArrayAndDestroy(t *testing.T) { t.Parallel() - inter := parseCheckAndInterpret(t, ` - var destroys = 0 + var events []*interpreter.CompositeValue + inter, err := parseCheckAndInterpretWithOptions(t, ` resource Foo { + event ResourceDestroyed(bar: Int = self.bar) var bar: Int init(bar: Int) { @@ -6505,14 +6506,15 @@ func TestInterpretResourceMoveInArrayAndDestroy(t *testing.T) { destroy foos return bar } - `) - - AssertValuesEqual( - t, - inter, - interpreter.NewUnmeteredIntValueFromInt64(0), - inter.Globals.Get("destroys").GetValue(), - ) + `, ParseCheckAndInterpretOptions{ + Config: &interpreter.Config{ + OnEventEmitted: func(inter *interpreter.Interpreter, locationRange interpreter.LocationRange, event *interpreter.CompositeValue, eventType *sema.CompositeType) error { + events = append(events, event) + return nil + }, + }, + }) + require.NoError(t, err) value, err := inter.Invoke("test") require.NoError(t, err) @@ -6524,17 +6526,22 @@ func TestInterpretResourceMoveInArrayAndDestroy(t *testing.T) { value, ) - // DestructorTODO: replace with test for destruction event + require.Len(t, events, 2) + require.Equal(t, events[0].QualifiedIdentifier, "Foo.ResourceDestroyed") + require.Equal(t, events[0].GetField(inter, interpreter.EmptyLocationRange, "bar"), interpreter.NewIntValueFromInt64(nil, 1)) + require.Equal(t, events[1].QualifiedIdentifier, "Foo.ResourceDestroyed") + require.Equal(t, events[1].GetField(inter, interpreter.EmptyLocationRange, "bar"), interpreter.NewIntValueFromInt64(nil, 2)) } func TestInterpretResourceMoveInDictionaryAndDestroy(t *testing.T) { t.Parallel() - inter := parseCheckAndInterpret(t, ` - var destroys = 0 + var events []*interpreter.CompositeValue + inter, err := parseCheckAndInterpretWithOptions(t, ` resource Foo { + event ResourceDestroyed(bar: Int = self.bar) var bar: Int init(bar: Int) { @@ -6548,19 +6555,24 @@ func TestInterpretResourceMoveInDictionaryAndDestroy(t *testing.T) { let foos <- {"foo1": <-foo1, "foo2": <-foo2} destroy foos } - `) - - RequireValuesEqual( - t, - inter, - interpreter.NewUnmeteredIntValueFromInt64(0), - inter.Globals.Get("destroys").GetValue(), - ) + `, ParseCheckAndInterpretOptions{ + Config: &interpreter.Config{ + OnEventEmitted: func(inter *interpreter.Interpreter, locationRange interpreter.LocationRange, event *interpreter.CompositeValue, eventType *sema.CompositeType) error { + events = append(events, event) + return nil + }, + }, + }) + require.NoError(t, err) - _, err := inter.Invoke("test") + _, err = inter.Invoke("test") require.NoError(t, err) - // DestructorTODO: replace with test for destruction event + require.Len(t, events, 2) + require.Equal(t, events[0].QualifiedIdentifier, "Foo.ResourceDestroyed") + require.Equal(t, events[0].GetField(inter, interpreter.EmptyLocationRange, "bar"), interpreter.NewIntValueFromInt64(nil, 1)) + require.Equal(t, events[1].QualifiedIdentifier, "Foo.ResourceDestroyed") + require.Equal(t, events[1].GetField(inter, interpreter.EmptyLocationRange, "bar"), interpreter.NewIntValueFromInt64(nil, 2)) } func TestInterpretClosure(t *testing.T) { @@ -6827,10 +6839,21 @@ func TestInterpretResourceDestroyExpressionNestedResources(t *testing.T) { t.Parallel() - inter := parseCheckAndInterpret(t, ` - resource B {} + var events []*interpreter.CompositeValue + + inter, err := parseCheckAndInterpretWithOptions(t, ` + resource B { + var foo: Int + event ResourceDestroyed(foo: Int = self.foo) + + init() { + self.foo = 5 + } + } resource A { + event ResourceDestroyed(foo: Int = self.b.foo) + let b: @B init(b: @B) { @@ -6843,88 +6866,153 @@ func TestInterpretResourceDestroyExpressionNestedResources(t *testing.T) { let a <- create A(b: <-b) destroy a } - `) + `, ParseCheckAndInterpretOptions{ + Config: &interpreter.Config{ + OnEventEmitted: func(inter *interpreter.Interpreter, locationRange interpreter.LocationRange, event *interpreter.CompositeValue, eventType *sema.CompositeType) error { + events = append(events, event) + return nil + }, + }, + }) + require.NoError(t, err) - _, err := inter.Invoke("test") + _, err = inter.Invoke("test") require.NoError(t, err) - // DestructorTODO: replace with test for destruction event of both A and B + require.Len(t, events, 2) + require.Equal(t, events[0].QualifiedIdentifier, "B.ResourceDestroyed") + require.Equal(t, events[0].GetField(inter, interpreter.EmptyLocationRange, "foo"), interpreter.NewIntValueFromInt64(nil, 5)) + require.Equal(t, events[1].QualifiedIdentifier, "A.ResourceDestroyed") + require.Equal(t, events[1].GetField(inter, interpreter.EmptyLocationRange, "foo"), interpreter.NewIntValueFromInt64(nil, 5)) } func TestInterpretResourceDestroyArray(t *testing.T) { t.Parallel() - inter := parseCheckAndInterpret(t, ` - resource R {} + var events []*interpreter.CompositeValue + + inter, err := parseCheckAndInterpretWithOptions(t, ` + resource R { + event ResourceDestroyed() + } fun test() { let rs <- [<-create R(), <-create R()] destroy rs } - `) + `, ParseCheckAndInterpretOptions{ + Config: &interpreter.Config{ + OnEventEmitted: func(inter *interpreter.Interpreter, locationRange interpreter.LocationRange, event *interpreter.CompositeValue, eventType *sema.CompositeType) error { + events = append(events, event) + return nil + }, + }, + }) + require.NoError(t, err) - _, err := inter.Invoke("test") + _, err = inter.Invoke("test") require.NoError(t, err) - // DestructorTODO: replace with test for destruction event emitted twice + require.Len(t, events, 2) + require.Equal(t, events[0].QualifiedIdentifier, "R.ResourceDestroyed") + require.Equal(t, events[1].QualifiedIdentifier, "R.ResourceDestroyed") } func TestInterpretResourceDestroyDictionary(t *testing.T) { t.Parallel() - inter := parseCheckAndInterpret(t, ` - resource R { } + var events []*interpreter.CompositeValue + + inter, err := parseCheckAndInterpretWithOptions(t, ` + resource R { + event ResourceDestroyed() + } fun test() { let rs <- {"r1": <-create R(), "r2": <-create R()} destroy rs } - `) + `, ParseCheckAndInterpretOptions{ + Config: &interpreter.Config{ + OnEventEmitted: func(inter *interpreter.Interpreter, locationRange interpreter.LocationRange, event *interpreter.CompositeValue, eventType *sema.CompositeType) error { + events = append(events, event) + return nil + }, + }, + }) + require.NoError(t, err) - _, err := inter.Invoke("test") + _, err = inter.Invoke("test") require.NoError(t, err) - // DestructorTODO: replace with test for destruction event emitted twice + require.Len(t, events, 2) + require.Equal(t, events[0].QualifiedIdentifier, "R.ResourceDestroyed") + require.Equal(t, events[1].QualifiedIdentifier, "R.ResourceDestroyed") } func TestInterpretResourceDestroyOptionalSome(t *testing.T) { t.Parallel() - inter := parseCheckAndInterpret(t, ` - resource R { } + var events []*interpreter.CompositeValue + + inter, err := parseCheckAndInterpretWithOptions(t, ` + resource R { + event ResourceDestroyed() + } fun test() { let maybeR: @R? <- create R() destroy maybeR } - `) + `, ParseCheckAndInterpretOptions{ + Config: &interpreter.Config{ + OnEventEmitted: func(inter *interpreter.Interpreter, locationRange interpreter.LocationRange, event *interpreter.CompositeValue, eventType *sema.CompositeType) error { + events = append(events, event) + return nil + }, + }, + }) + require.NoError(t, err) - _, err := inter.Invoke("test") + _, err = inter.Invoke("test") require.NoError(t, err) - // DestructorTODO: replace with test for destruction event + require.Len(t, events, 1) + require.Equal(t, events[0].QualifiedIdentifier, "R.ResourceDestroyed") } func TestInterpretResourceDestroyOptionalNil(t *testing.T) { t.Parallel() - inter := parseCheckAndInterpret(t, ` - resource R {} + var events []*interpreter.CompositeValue + + inter, err := parseCheckAndInterpretWithOptions(t, ` + resource R { + event ResourceDestroyed() + } fun test() { let maybeR: @R? <- nil destroy maybeR } - `) + `, ParseCheckAndInterpretOptions{ + Config: &interpreter.Config{ + OnEventEmitted: func(inter *interpreter.Interpreter, locationRange interpreter.LocationRange, event *interpreter.CompositeValue, eventType *sema.CompositeType) error { + events = append(events, event) + return nil + }, + }, + }) + require.NoError(t, err) - _, err := inter.Invoke("test") + _, err = inter.Invoke("test") require.NoError(t, err) - // DestructorTODO: replace with test for destruction event not emitted + require.Len(t, events, 0) } // TestInterpretInterfaceInitializer tests that the interface's initializer @@ -9331,39 +9419,15 @@ func TestInterpretNestedDestroy(t *testing.T) { t.Parallel() - var logs []string - - logFunction := stdlib.NewStandardLibraryFunction( - "log", - &sema.FunctionType{ - Parameters: []sema.Parameter{ - { - Label: sema.ArgumentLabelNotRequired, - Identifier: "value", - TypeAnnotation: sema.AnyStructTypeAnnotation, - }, - }, - ReturnTypeAnnotation: sema.VoidTypeAnnotation, - }, - ``, - func(invocation interpreter.Invocation) interpreter.Value { - message := invocation.Arguments[0].String() - logs = append(logs, message) - return interpreter.Void - }, - ) - - baseValueActivation := sema.NewVariableActivation(sema.BaseValueActivation) - baseValueActivation.DeclareValue(logFunction) - - baseActivation := activations.NewActivation(nil, interpreter.BaseActivation) - interpreter.Declare(baseActivation, logFunction) + var events []*interpreter.CompositeValue inter, err := parseCheckAndInterpretWithOptions(t, ` resource B { let id: Int + event ResourceDestroyed(id: Int = self.id) + init(_ id: Int){ self.id = id } @@ -9373,6 +9437,8 @@ func TestInterpretNestedDestroy(t *testing.T) { let id: Int let bs: @[B] + event ResourceDestroyed(id: Int = self.id, bCount: Int = self.bs.length) + init(_ id: Int){ self.id = id self.bs <- [] @@ -9391,30 +9457,37 @@ func TestInterpretNestedDestroy(t *testing.T) { destroy a } - `, - ParseCheckAndInterpretOptions{ + `, ParseCheckAndInterpretOptions{ Config: &interpreter.Config{ - BaseActivation: baseActivation, - }, - CheckerConfig: &sema.Config{ - BaseValueActivation: baseValueActivation, + OnEventEmitted: func(inter *interpreter.Interpreter, locationRange interpreter.LocationRange, event *interpreter.CompositeValue, eventType *sema.CompositeType) error { + events = append(events, event) + return nil + }, }, - HandleCheckerError: nil, - }, - ) + }) + require.NoError(t, err) value, err := inter.Invoke("test") require.NoError(t, err) + require.Len(t, events, 4) + require.Equal(t, events[0].QualifiedIdentifier, "B.ResourceDestroyed") + require.Equal(t, events[0].GetField(inter, interpreter.EmptyLocationRange, "id"), interpreter.NewIntValueFromInt64(nil, 2)) + require.Equal(t, events[1].QualifiedIdentifier, "B.ResourceDestroyed") + require.Equal(t, events[1].GetField(inter, interpreter.EmptyLocationRange, "id"), interpreter.NewIntValueFromInt64(nil, 3)) + require.Equal(t, events[2].QualifiedIdentifier, "B.ResourceDestroyed") + require.Equal(t, events[2].GetField(inter, interpreter.EmptyLocationRange, "id"), interpreter.NewIntValueFromInt64(nil, 4)) + require.Equal(t, events[3].QualifiedIdentifier, "A.ResourceDestroyed") + require.Equal(t, events[3].GetField(inter, interpreter.EmptyLocationRange, "id"), interpreter.NewIntValueFromInt64(nil, 1)) + require.Equal(t, events[3].GetField(inter, interpreter.EmptyLocationRange, "bCount"), interpreter.NewIntValueFromInt64(nil, 3)) + AssertValuesEqual( t, inter, interpreter.Void, value, ) - - // DestructorTODO: replace with test for destruction event for A and B } // TestInterpretInternalAssignment ensures that a modification of an "internal" value From b8954d4f50447a4c7096ea3cc98eb73ec73f674e Mon Sep 17 00:00:00 2001 From: Daniel Sainati Date: Tue, 26 Sep 2023 13:05:41 -0400 Subject: [PATCH 03/16] fix attachment base values --- runtime/interpreter/value.go | 3 + runtime/tests/checker/events_test.go | 70 +++++++++++++++++++ runtime/tests/interpreter/attachments_test.go | 57 +++++++++++++++ 3 files changed, 130 insertions(+) diff --git a/runtime/interpreter/value.go b/runtime/interpreter/value.go index 5fe722669c..2126240eba 100644 --- a/runtime/interpreter/value.go +++ b/runtime/interpreter/value.go @@ -16505,6 +16505,9 @@ func (v *CompositeValue) Destroy(interpreter *Interpreter, locationRange Locatio // destroy every nested resource in this composite; note that this iteration includes attachments v.ForEachField(interpreter, func(_ string, fieldValue Value) bool { + if compositeFieldValue, ok := fieldValue.(*CompositeValue); ok && compositeFieldValue.Kind == common.CompositeKindAttachment { + compositeFieldValue.setBaseValue(interpreter, v, attachmentBaseAuthorization(interpreter, compositeFieldValue)) + } maybeDestroy(interpreter, locationRange, fieldValue) return true }) diff --git a/runtime/tests/checker/events_test.go b/runtime/tests/checker/events_test.go index ae6584561f..080c69d50d 100644 --- a/runtime/tests/checker/events_test.go +++ b/runtime/tests/checker/events_test.go @@ -1042,4 +1042,74 @@ func TestCheckDefaultEventParamChecking(t *testing.T) { require.NoError(t, err) }) + t.Run("attachment with entitled base", func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, ` + entitlement E + + attachment A for R { + event ResourceDestroyed(name: Int = base.field) + } + + resource R { + access(E) let field : Int + + init() { + self.field = 3 + } + } + `) + errs := RequireCheckerErrors(t, err, 1) + assert.IsType(t, &sema.InvalidAccessError{}, errs[0]) + }) + + t.Run("attachment with entitled base allowed", func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, ` + entitlement E + + attachment A for R { + require entitlement E + event ResourceDestroyed(name: Int = base.field) + } + + resource R { + access(E) let field : Int + + init() { + self.field = 3 + } + } + `) + require.NoError(t, err) + }) + + t.Run("attachment with entitled self", func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, ` + entitlement E + + entitlement mapping M { + E -> E + } + + access(M) attachment A for R { + access(E) let field : Int + event ResourceDestroyed(name: Int = self.field) + init() { + self.field = 3 + } + } + + resource R {} + `) + require.NoError(t, err) + }) + } diff --git a/runtime/tests/interpreter/attachments_test.go b/runtime/tests/interpreter/attachments_test.go index a6894e5a75..16aeee2fcb 100644 --- a/runtime/tests/interpreter/attachments_test.go +++ b/runtime/tests/interpreter/attachments_test.go @@ -1452,6 +1452,63 @@ func TestInterpretAttachmentDestructor(t *testing.T) { require.Equal(t, events[1].QualifiedIdentifier, "R2.ResourceDestroyed") require.Equal(t, events[2].QualifiedIdentifier, "A.ResourceDestroyed") }) + + t.Run("attachment default args properly scoped", func(t *testing.T) { + + t.Parallel() + + var events []*interpreter.CompositeValue + + inter, err := parseCheckAndInterpretWithOptions(t, ` + resource R { + var foo: String + event ResourceDestroyed() + init() { + self.foo = "baz" + } + fun setFoo(arg: String) { + self.foo = arg + } + } + attachment A for R { + var bar: Int + event ResourceDestroyed(foo: String = base.foo, bar: Int = self.bar) + init() { + self.bar = 1 + } + fun setBar(arg: Int) { + self.bar = arg + } + } + fun test() { + let r <- create R() + let r2 <- attach A() to <-r + r2.setFoo(arg: "foo") + r2[A]!.setBar(arg: 2) + destroy r2 + } + `, ParseCheckAndInterpretOptions{ + Config: &interpreter.Config{ + OnEventEmitted: func(inter *interpreter.Interpreter, locationRange interpreter.LocationRange, event *interpreter.CompositeValue, eventType *sema.CompositeType) error { + events = append(events, event) + return nil + }, + }, + CheckerConfig: &sema.Config{ + AttachmentsEnabled: true, + }, + }) + require.NoError(t, err) + + _, err = inter.Invoke("test") + require.NoError(t, err) + + require.Len(t, events, 2) + require.Equal(t, events[0].QualifiedIdentifier, "A.ResourceDestroyed") + require.Equal(t, events[0].GetField(inter, interpreter.EmptyLocationRange, "foo"), interpreter.NewUnmeteredStringValue("foo")) + require.Equal(t, events[0].GetField(inter, interpreter.EmptyLocationRange, "bar"), interpreter.NewIntValueFromInt64(nil, 2)) + require.Equal(t, events[1].QualifiedIdentifier, "R.ResourceDestroyed") + }) } func TestInterpretAttachmentResourceReferenceInvalidation(t *testing.T) { From e40c4907cebd888601441fed9506b0b29baf4a7f Mon Sep 17 00:00:00 2001 From: Daniel Sainati Date: Tue, 26 Sep 2023 16:51:47 -0400 Subject: [PATCH 04/16] fix scoping across interpreters --- runtime/interpreter/interpreter.go | 65 ++++++++--- runtime/interpreter/value.go | 14 +-- runtime/resourcedictionary_test.go | 26 ++++- runtime/runtime_test.go | 169 +++++++++++++++++++++++++++-- 4 files changed, 238 insertions(+), 36 deletions(-) diff --git a/runtime/interpreter/interpreter.go b/runtime/interpreter/interpreter.go index 1431602436..f33a7f2518 100644 --- a/runtime/interpreter/interpreter.go +++ b/runtime/interpreter/interpreter.go @@ -965,6 +965,47 @@ func (interpreter *Interpreter) declareAttachmentValue( return interpreter.declareCompositeValue(declaration, lexicalScope) } +// evaluates all the implicit default arguments to the default destroy event +// +// the handling of default arguments makes a number of assumptions to simplify the implementation; +// namely that a) all default arguments are lazily evaluated at the site of the invocation, +// b) that either all the parameters or none of the parameters of a function have default arguments, +// and c) functions cannot currently be explicitly invoked if they have default arguments +// +// if we plan to generalize this further, we will need to relax those assumptions +func (interpreter *Interpreter) evaluateDefaultDestroyEvent( + containerComposite *CompositeValue, + compositeDecl *ast.CompositeDeclaration, + compositeType *sema.CompositeType, + locationRange LocationRange, +) (arguments []Value) { + parameters := compositeDecl.DeclarationMembers().Initializers()[0].FunctionDeclaration.ParameterList.Parameters + + interpreter.activations.PushNewWithParent(interpreter.activations.CurrentOrNew()) + defer interpreter.activations.Pop() + + var self MemberAccessibleValue = containerComposite + if containerComposite.Kind == common.CompositeKindAttachment { + var base *EphemeralReferenceValue + base, self = attachmentBaseAndSelfValues(interpreter, containerComposite) + interpreter.declareVariable(sema.BaseIdentifier, base) + } + interpreter.declareVariable(sema.SelfIdentifier, self) + + for _, parameter := range parameters { + // lazily evaluate the default argument expressions + // note that we must evaluate them in the interpreter context that existed when the event + // was defined (i.e. the contract defining the resource) rather than the interpreter context + // that exists when the resource is destroyed. We accomplish this by using the original interpreter of the + // composite declaration, rather than the interpreter of the destroy expression + + defaultArg := interpreter.evalExpression(parameter.DefaultArgument) + arguments = append(arguments, defaultArg) + } + + return +} + // declareCompositeValue creates and declares the value for // the composite declaration. // @@ -1101,27 +1142,21 @@ func (interpreter *Interpreter) declareNonEnumCompositeValue( locationRange := invocation.LocationRange self := *invocation.Self - // the handling of default arguments makes a number of assumptions to simplify the implementation; - // namely that a) all default arguments are lazily evaluated at the site of the invocation, - // b) that either all the parameters or none of the parameters of a function have default arguments, - // and c) functions cannot currently be explicitly invoked if they have default arguments - // if we plan to generalize this further, we will need to relax those assumptions if len(compositeType.ConstructorParameters) < 1 { return nil } - // event intefaces do not exist + // event interfaces do not exist compositeDecl := declaration.(*ast.CompositeDeclaration) if compositeDecl.IsResourceDestructionDefaultEvent() { - parameters := compositeDecl.DeclarationMembers().Initializers()[0].FunctionDeclaration.ParameterList.Parameters - // if the first parameter has a default arg, all of them do, and the arguments list is empty - if parameters[0].DefaultArgument != nil { - // lazily evaluate the default argument expression in this context - for _, parameter := range parameters { - defaultArg := interpreter.evalExpression(parameter.DefaultArgument) - invocation.Arguments = append(invocation.Arguments, defaultArg) - } - } + // we implicitly pass the containing composite value as an argument to this invocation + containerComposite := invocation.Arguments[0].(*CompositeValue) + invocation.Arguments = interpreter.evaluateDefaultDestroyEvent( + containerComposite, + compositeDecl, + compositeType, + locationRange, + ) } for i, argument := range invocation.Arguments { diff --git a/runtime/interpreter/value.go b/runtime/interpreter/value.go index 2126240eba..34ed073de6 100644 --- a/runtime/interpreter/value.go +++ b/runtime/interpreter/value.go @@ -16472,19 +16472,15 @@ func (v *CompositeValue) Destroy(interpreter *Interpreter, locationRange Locatio // if the composite was recently loaded from storage v.InitializeFunctions(interpreter) if constructor := v.defaultDestroyEventConstructor(); constructor != nil { - var self MemberAccessibleValue = v - if v.Kind == common.CompositeKindAttachment { - var base *EphemeralReferenceValue - base, self = attachmentBaseAndSelfValues(interpreter, v) - interpreter.declareVariable(sema.BaseIdentifier, base) - } - interpreter.declareVariable(sema.SelfIdentifier, self) + + // pass the container value to the creation of the default event as an implicit argument, so that + // its fields are accessible in the body of the event constructor mockInvocation := NewInvocation( interpreter, nil, nil, nil, - []Value{}, + []Value{v}, []sema.Type{}, nil, locationRange, @@ -16492,6 +16488,8 @@ func (v *CompositeValue) Destroy(interpreter *Interpreter, locationRange Locatio event := constructor.invoke(mockInvocation).(*CompositeValue) eventType := interpreter.MustSemaTypeOfValue(event).(*sema.CompositeType) + + // emit the event once destruction is complete defer interpreter.emitEvent(event, eventType, locationRange) } diff --git a/runtime/resourcedictionary_test.go b/runtime/resourcedictionary_test.go index ab2380c0fc..75a61fb41b 100644 --- a/runtime/resourcedictionary_test.go +++ b/runtime/resourcedictionary_test.go @@ -38,6 +38,8 @@ const resourceDictionaryContract = ` access(all) resource R { + access(all) event ResourceDestroyed(value: Int = self.value) + access(all) var value: Int init(_ value: Int) { @@ -255,6 +257,7 @@ func TestRuntimeResourceDictionaryValues(t *testing.T) { `) loggedMessages = nil + events = nil err = runtime.ExecuteTransaction( Script{ @@ -274,8 +277,8 @@ func TestRuntimeResourceDictionaryValues(t *testing.T) { }, loggedMessages, ) - - // DestructorTODO: add test for destruction event of R + require.Len(t, events, 1) + require.Equal(t, events[0].String(), "A.000000000000cade.Test.R.ResourceDestroyed(value: 3)") // Remove the key @@ -294,6 +297,7 @@ func TestRuntimeResourceDictionaryValues(t *testing.T) { `) loggedMessages = nil + events = nil err = runtime.ExecuteTransaction( Script{ @@ -313,8 +317,8 @@ func TestRuntimeResourceDictionaryValues(t *testing.T) { }, loggedMessages, ) - - // DestructorTODO: add test for destruction event of R + require.Len(t, events, 1) + require.Equal(t, events[0].String(), "A.000000000000cade.Test.R.ResourceDestroyed(value: 4)") // Read the deleted key @@ -354,6 +358,7 @@ func TestRuntimeResourceDictionaryValues(t *testing.T) { `) loggedMessages = nil + events = nil err = runtime.ExecuteTransaction( Script{ @@ -372,8 +377,9 @@ func TestRuntimeResourceDictionaryValues(t *testing.T) { }, loggedMessages, ) + require.Len(t, events, 1) + require.Equal(t, events[0].String(), "A.000000000000cade.Test.R.ResourceDestroyed(value: 1)") - // DestructorTODO: add test for destruction event of R } func TestRuntimeResourceDictionaryValues_Nested(t *testing.T) { @@ -972,7 +978,15 @@ func TestRuntimeResourceDictionaryValues_Destruction(t *testing.T) { ) require.NoError(t, err) - // DestructorTODO: replace with test for destruction event of R twice + require.Len(t, events, 3) + require.Equal(t, events[0].EventType.ID(), "flow.AccountContractAdded") + require.Equal(t, events[1].EventType.ID(), "A.0000000000000001.Test.R.ResourceDestroyed") + require.Equal(t, events[2].EventType.ID(), "A.0000000000000001.Test.R.ResourceDestroyed") + // one of the two needs to be 1, the other needs to be 2 + require.True(t, + (events[1].Fields[0].String() == "1" && events[2].Fields[0].String() == "2") || + (events[2].Fields[0].String() == "1" && events[1].Fields[0].String() == "2"), + ) } func TestRuntimeResourceDictionaryValues_Insertion(t *testing.T) { diff --git a/runtime/runtime_test.go b/runtime/runtime_test.go index cac4b463d7..af64dd54c8 100644 --- a/runtime/runtime_test.go +++ b/runtime/runtime_test.go @@ -3129,11 +3129,22 @@ func TestRuntimeStorageLoadedDestructionConcreteType(t *testing.T) { contract := []byte(` access(all) contract Test { - access(all) resource R {} + access(all) resource R { + access(self) var foo: Int + access(all) event ResourceDestroyed(foo: Int = self.foo) + init() { + self.foo = 0 + } + access(all) fun setFoo(_ arg: Int) { + self.foo = arg + } + } init() { // store nested resource in account on deployment - self.account.storage.save(<-create R(), to: /storage/r) + let r <-create R() + r.setFoo(3) + self.account.storage.save(<-r, to: /storage/r) } } `) @@ -3145,6 +3156,7 @@ func TestRuntimeStorageLoadedDestructionConcreteType(t *testing.T) { prepare(acct: auth(Storage) &Account) { let r <- acct.storage.load<@Test.R>(from: /storage/r) + r?.setFoo(6) destroy r } } @@ -3153,6 +3165,7 @@ func TestRuntimeStorageLoadedDestructionConcreteType(t *testing.T) { deploy := DeploymentTransaction("Test", contract) var accountCode []byte + var events []cadence.Event runtimeInterface := &TestRuntimeInterface{ OnGetCode: func(_ Location) (bytes []byte, err error) { @@ -3170,7 +3183,10 @@ func TestRuntimeStorageLoadedDestructionConcreteType(t *testing.T) { accountCode = code return nil }, - OnEmitEvent: func(event cadence.Event) error { return nil }, + OnEmitEvent: func(event cadence.Event) error { + events = append(events, event) + return nil + }, } nextTransactionLocation := NewTransactionLocationGenerator() @@ -3198,8 +3214,139 @@ func TestRuntimeStorageLoadedDestructionConcreteType(t *testing.T) { }) require.NoError(t, err) - // DestructorTODO: Assert default event is emitted here + require.Len(t, events, 2) + require.Equal(t, events[0].EventType.ID(), "flow.AccountContractAdded") + require.Equal(t, events[1].String(), "A.0000000000000001.Test.R.ResourceDestroyed(foo: 6)") +} + +func TestRuntimeStorageLoadedDestructionConcreteTypeWithAttachment(t *testing.T) { + + t.Parallel() + runtime := NewTestInterpreterRuntimeWithAttachments() + + addressValue := Address{ + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, + } + + attachmentContract := []byte(` + import Test from 0x01 + + access(all) contract TestAttach { + access(all) attachment A for Test.R { + access(all) event ResourceDestroyed(foo: Int = base.foo) + } + } + `) + + contract := []byte(` + access(all) contract Test { + + access(all) resource R { + access(all) var foo: Int + access(all) event ResourceDestroyed(foo: Int = self.foo) + init() { + self.foo = 0 + } + access(all) fun setFoo(_ arg: Int) { + self.foo = arg + } + } + + init() { + // store nested resource in account on deployment + let r <-create R() + r.setFoo(3) + self.account.storage.save(<-r, to: /storage/r) + } + } + `) + + tx := []byte(` + import Test from 0x01 + import TestAttach from 0x01 + + transaction { + + prepare(acct: auth(Storage) &Account) { + let r <- acct.storage.load<@Test.R>(from: /storage/r)! + let withAttachment <- attach TestAttach.A() to <-r + withAttachment.setFoo(6) + destroy withAttachment + } + } + `) + + deploy := DeploymentTransaction("Test", contract) + deployAttachment := DeploymentTransaction("TestAttach", attachmentContract) + + accountCodes := map[Location][]byte{} + var events []cadence.Event + + runtimeInterface := &TestRuntimeInterface{ + OnGetCode: func(location Location) (bytes []byte, err error) { + return accountCodes[location], nil + }, + Storage: NewTestLedger(nil, nil), + OnGetSigningAccounts: func() ([]Address, error) { + return []Address{addressValue}, nil + }, + OnResolveLocation: NewSingleIdentifierLocationResolver(t), + OnGetAccountContractCode: func(location common.AddressLocation) (code []byte, err error) { + return accountCodes[location], nil + }, + OnUpdateAccountContractCode: func(location common.AddressLocation, code []byte) error { + accountCodes[location] = code + return nil + }, + OnCreateAccount: func(payer Address) (address Address, err error) { + return addressValue, nil + }, + OnEmitEvent: func(event cadence.Event) error { + events = append(events, event) + return nil + }, + } + + nextTransactionLocation := NewTransactionLocationGenerator() + + err := runtime.ExecuteTransaction( + Script{ + Source: deploy, + }, + Context{ + Interface: runtimeInterface, + Location: nextTransactionLocation(), + }, + ) + require.NoError(t, err) + + err = runtime.ExecuteTransaction( + Script{ + Source: deployAttachment, + }, + Context{ + Interface: runtimeInterface, + Location: nextTransactionLocation(), + }, + ) + require.NoError(t, err) + + err = runtime.ExecuteTransaction( + Script{ + Source: tx, + }, + Context{ + Interface: runtimeInterface, + Location: nextTransactionLocation(), + }) + require.NoError(t, err) + + require.Len(t, events, 4) + require.Equal(t, events[0].EventType.ID(), "flow.AccountContractAdded") + require.Equal(t, events[1].EventType.ID(), "flow.AccountContractAdded") + require.Equal(t, events[2].String(), "A.0000000000000001.TestAttach.A.ResourceDestroyed(foo: 6)") + require.Equal(t, events[3].String(), "A.0000000000000001.Test.R.ResourceDestroyed(foo: 6)") } func TestRuntimeStorageLoadedDestructionAnyResource(t *testing.T) { @@ -3214,7 +3361,9 @@ func TestRuntimeStorageLoadedDestructionAnyResource(t *testing.T) { contract := []byte(` access(all) contract Test { - access(all) resource R {} + access(all) resource R { + access(all) event ResourceDestroyed() + } init() { // store nested resource in account on deployment @@ -3239,6 +3388,7 @@ func TestRuntimeStorageLoadedDestructionAnyResource(t *testing.T) { deploy := DeploymentTransaction("Test", contract) var accountCode []byte + var events []cadence.Event runtimeInterface := &TestRuntimeInterface{ OnGetCode: func(_ Location) (bytes []byte, err error) { @@ -3256,7 +3406,10 @@ func TestRuntimeStorageLoadedDestructionAnyResource(t *testing.T) { accountCode = code return nil }, - OnEmitEvent: func(event cadence.Event) error { return nil }, + OnEmitEvent: func(event cadence.Event) error { + events = append(events, event) + return nil + }, } nextTransactionLocation := NewTransactionLocationGenerator() @@ -3285,7 +3438,9 @@ func TestRuntimeStorageLoadedDestructionAnyResource(t *testing.T) { ) require.NoError(t, err) - // DestructorTODO: Assert default event is emitted here + require.Len(t, events, 2) + require.Equal(t, events[0].EventType.ID(), "flow.AccountContractAdded") + require.Equal(t, events[1].String(), "A.0000000000000001.Test.R.ResourceDestroyed()") } func TestRuntimeStorageLoadedDestructionAfterRemoval(t *testing.T) { From 24e61641efcfedb7442d57399fb631dc6caa21f0 Mon Sep 17 00:00:00 2001 From: Daniel Sainati Date: Tue, 26 Sep 2023 16:55:22 -0400 Subject: [PATCH 05/16] lint --- runtime/interpreter/interpreter_invocation.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/runtime/interpreter/interpreter_invocation.go b/runtime/interpreter/interpreter_invocation.go index 2019df8a52..8f4fb6dbed 100644 --- a/runtime/interpreter/interpreter_invocation.go +++ b/runtime/interpreter/interpreter_invocation.go @@ -185,14 +185,12 @@ func (interpreter *Interpreter) invokeInterpretedFunctionActivated( ) } -// bindParameterArguments binds the argument values to the given parameters. +// bindParameterArguments binds the argument values to the given parameters func (interpreter *Interpreter) bindParameterArguments( parameterList *ast.ParameterList, arguments []Value, ) { - parameters := parameterList.Parameters - - for parameterIndex, parameter := range parameters { + for parameterIndex, parameter := range parameterList.Parameters { argument := arguments[parameterIndex] interpreter.declareVariable(parameter.Identifier.Identifier, argument) } From 0d9bf8ccb653a2fc6b0e5dda592b9e2ff0376d73 Mon Sep 17 00:00:00 2001 From: Daniel Sainati Date: Wed, 27 Sep 2023 12:23:01 -0400 Subject: [PATCH 06/16] emit interface events as well --- runtime/interpreter/interpreter.go | 39 +++- runtime/interpreter/value.go | 23 ++- runtime/runtime_test.go | 134 ++++++++++++ runtime/tests/interpreter/resources_test.go | 214 ++++++++++++++++++++ 4 files changed, 394 insertions(+), 16 deletions(-) diff --git a/runtime/interpreter/interpreter.go b/runtime/interpreter/interpreter.go index f33a7f2518..18b524cfd5 100644 --- a/runtime/interpreter/interpreter.go +++ b/runtime/interpreter/interpreter.go @@ -178,9 +178,10 @@ type FunctionWrapper = func(inner FunctionValue) FunctionValue // These are "branch" nodes in the call chain, and are function wrappers, // i.e. they wrap the functions / function wrappers that inherit them. type WrapperCode struct { - InitializerFunctionWrapper FunctionWrapper - FunctionWrappers map[string]FunctionWrapper - Functions map[string]FunctionValue + InitializerFunctionWrapper FunctionWrapper + FunctionWrappers map[string]FunctionWrapper + Functions map[string]FunctionValue + DefaultDestroyEventConstructor FunctionValue } // TypeCodes is the value which stores the "prepared" / "callable" "code" @@ -1181,10 +1182,10 @@ func (interpreter *Interpreter) declareNonEnumCompositeValue( functions := interpreter.compositeFunctions(declaration, lexicalScope) if destroyEventConstructor != nil { - functions[resourceDefaultDestroyEventName] = destroyEventConstructor + functions[resourceDefaultDestroyEventName(compositeType)] = destroyEventConstructor } - wrapFunctions := func(code WrapperCode) { + wrapFunctions := func(ty *sema.InterfaceType, code WrapperCode) { // Wrap initializer @@ -1220,12 +1221,16 @@ func (interpreter *Interpreter) declareNonEnumCompositeValue( for name, functionWrapper := range code.FunctionWrappers { //nolint:maprange functions[name] = functionWrapper(functions[name]) } + + if code.DefaultDestroyEventConstructor != nil { + functions[resourceDefaultDestroyEventName(ty)] = code.DefaultDestroyEventConstructor + } } conformances := compositeType.EffectiveInterfaceConformances() for i := len(conformances) - 1; i >= 0; i-- { conformance := conformances[i].InterfaceType - wrapFunctions(interpreter.SharedState.typeCodes.InterfaceCodes[conformance.ID()]) + wrapFunctions(conformance, interpreter.SharedState.typeCodes.InterfaceCodes[conformance.ID()]) } interpreter.SharedState.typeCodes.CompositeCodes[compositeType.ID()] = CompositeTypeCode{ @@ -2228,13 +2233,29 @@ func (interpreter *Interpreter) declareInterface( interfaceType.InitializerParameters, lexicalScope, ) + + var defaultDestroyEventConstructor FunctionValue + for _, nestedCompositeDeclaration := range declaration.Members.Composites() { + // statically we know there is at most one of these + if nestedCompositeDeclaration.IsResourceDestructionDefaultEvent() { + var nestedVariable *Variable + lexicalScope, nestedVariable = interpreter.declareCompositeValue( + nestedCompositeDeclaration, + lexicalScope, + ) + defaultDestroyEventConstructor = nestedVariable.GetValue().(FunctionValue) + break + } + } + functionWrappers := interpreter.functionWrappers(declaration.Members, lexicalScope) defaultFunctions := interpreter.defaultFunctions(declaration.Members, lexicalScope) interpreter.SharedState.typeCodes.InterfaceCodes[typeID] = WrapperCode{ - InitializerFunctionWrapper: initializerFunctionWrapper, - FunctionWrappers: functionWrappers, - Functions: defaultFunctions, + InitializerFunctionWrapper: initializerFunctionWrapper, + FunctionWrappers: functionWrappers, + Functions: defaultFunctions, + DefaultDestroyEventConstructor: defaultDestroyEventConstructor, } } diff --git a/runtime/interpreter/value.go b/runtime/interpreter/value.go index 34ed073de6..f053cfa4ab 100644 --- a/runtime/interpreter/value.go +++ b/runtime/interpreter/value.go @@ -16232,7 +16232,7 @@ type CompositeField struct { } const unrepresentableNamePrefix = "$" -const resourceDefaultDestroyEventName = unrepresentableNamePrefix + "ResourceDestroyed" +const resourceDefaultDestroyEventPrefix = "ResourceDestroyed" + unrepresentableNamePrefix var _ TypeIndexableValue = &CompositeValue{} @@ -16428,8 +16428,17 @@ func (v *CompositeValue) IsDestroyed() bool { return v.isDestroyed } -func (v *CompositeValue) defaultDestroyEventConstructor() FunctionValue { - return v.Functions[resourceDefaultDestroyEventName] +func resourceDefaultDestroyEventName(t sema.ContainerType) string { + return resourceDefaultDestroyEventPrefix + string(t.ID()) +} + +func (v *CompositeValue) defaultDestroyEventConstructors() (constructors []FunctionValue) { + for name, f := range v.Functions { + if strings.HasPrefix(name, resourceDefaultDestroyEventPrefix) { + constructors = append(constructors, f) + } + } + return } func (v *CompositeValue) Destroy(interpreter *Interpreter, locationRange LocationRange) { @@ -16463,15 +16472,15 @@ func (v *CompositeValue) Destroy(interpreter *Interpreter, locationRange Locatio } // before actually performing the destruction (i.e. so that any fields are still available), - // compute the default arguments of the default destruction event (if it exists). However, - // wait until after the destruction completes to actually emit the event, so that the correct order + // compute the default arguments of the default destruction events (if any exist). However, + // wait until after the destruction completes to actually emit the events, so that the correct order // is preserved and nested resource destroy events happen first - // the default destroy event constructor is encoded as a function on the resource (with an unrepresentable name) + // default destroy event constructors are encoded as functions on the resource (with an unrepresentable name) // so that we can leverage existing atree encoding and decoding. However, we need to make sure functions are initialized // if the composite was recently loaded from storage v.InitializeFunctions(interpreter) - if constructor := v.defaultDestroyEventConstructor(); constructor != nil { + for _, constructor := range v.defaultDestroyEventConstructors() { // pass the container value to the creation of the default event as an implicit argument, so that // its fields are accessible in the body of the event constructor diff --git a/runtime/runtime_test.go b/runtime/runtime_test.go index af64dd54c8..e015f6544b 100644 --- a/runtime/runtime_test.go +++ b/runtime/runtime_test.go @@ -3349,6 +3349,140 @@ func TestRuntimeStorageLoadedDestructionConcreteTypeWithAttachment(t *testing.T) require.Equal(t, events[3].String(), "A.0000000000000001.Test.R.ResourceDestroyed(foo: 6)") } +func TestRuntimeStorageLoadedDestructionConcreteTypeWithAttachmentUnloadedContract(t *testing.T) { + + t.Parallel() + + runtime := NewTestInterpreterRuntimeWithAttachments() + + addressValue := Address{ + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, + } + + attachmentContract := []byte(` + access(all) contract TestAttach { + access(all) resource interface I { + access(all) var foo: Int + access(all) event ResourceDestroyed(foo: Int = self.foo) + } + + access(all) attachment A for I { + access(all) event ResourceDestroyed(foo: Int = base.foo) + } + } + `) + + contract := []byte(` + import TestAttach from 0x01 + + access(all) contract Test { + + access(all) resource R: TestAttach.I { + access(all) var foo: Int + access(all) event ResourceDestroyed(foo: Int = self.foo) + init() { + self.foo = 0 + } + access(all) fun setFoo(_ arg: Int) { + self.foo = arg + } + } + + init() { + // store nested resource in account on deployment + let r <- attach TestAttach.A() to <-create R() + r.setFoo(3) + self.account.storage.save(<-r, to: /storage/r) + } + } + `) + + tx := []byte(` + import Test from 0x01 + + transaction { + + prepare(acct: auth(Storage) &Account) { + let r <- acct.storage.load<@Test.R>(from: /storage/r)! + r.setFoo(6) + destroy r + } + } + `) + + deploy := DeploymentTransaction("Test", contract) + deployAttachment := DeploymentTransaction("TestAttach", attachmentContract) + + accountCodes := map[Location][]byte{} + var events []cadence.Event + + runtimeInterface := &TestRuntimeInterface{ + OnGetCode: func(location Location) (bytes []byte, err error) { + return accountCodes[location], nil + }, + Storage: NewTestLedger(nil, nil), + OnGetSigningAccounts: func() ([]Address, error) { + return []Address{addressValue}, nil + }, + OnResolveLocation: NewSingleIdentifierLocationResolver(t), + OnGetAccountContractCode: func(location common.AddressLocation) (code []byte, err error) { + return accountCodes[location], nil + }, + OnUpdateAccountContractCode: func(location common.AddressLocation, code []byte) error { + accountCodes[location] = code + return nil + }, + OnCreateAccount: func(payer Address) (address Address, err error) { + return addressValue, nil + }, + OnEmitEvent: func(event cadence.Event) error { + events = append(events, event) + return nil + }, + } + + nextTransactionLocation := NewTransactionLocationGenerator() + + err := runtime.ExecuteTransaction( + Script{ + Source: deployAttachment, + }, + Context{ + Interface: runtimeInterface, + Location: nextTransactionLocation(), + }, + ) + require.NoError(t, err) + + err = runtime.ExecuteTransaction( + Script{ + Source: deploy, + }, + Context{ + Interface: runtimeInterface, + Location: nextTransactionLocation(), + }, + ) + require.NoError(t, err) + + err = runtime.ExecuteTransaction( + Script{ + Source: tx, + }, + Context{ + Interface: runtimeInterface, + Location: nextTransactionLocation(), + }) + require.NoError(t, err) + + require.Len(t, events, 5) + require.Equal(t, events[0].EventType.ID(), "flow.AccountContractAdded") + require.Equal(t, events[1].EventType.ID(), "flow.AccountContractAdded") + require.Equal(t, events[2].String(), "A.0000000000000001.TestAttach.A.ResourceDestroyed(foo: 6)") + require.Equal(t, events[3].String(), "A.0000000000000001.TestAttach.I.ResourceDestroyed(foo: 6)") + require.Equal(t, events[4].String(), "A.0000000000000001.Test.R.ResourceDestroyed(foo: 6)") +} + func TestRuntimeStorageLoadedDestructionAnyResource(t *testing.T) { t.Parallel() diff --git a/runtime/tests/interpreter/resources_test.go b/runtime/tests/interpreter/resources_test.go index 042f1c4f80..4c8f749a2a 100644 --- a/runtime/tests/interpreter/resources_test.go +++ b/runtime/tests/interpreter/resources_test.go @@ -2245,3 +2245,217 @@ func TestInterpretImplicitDestruction(t *testing.T) { require.NoError(t, err) }) } + +func TestInterpretResourceInterfaceDefaultDestroyEvent(t *testing.T) { + + t.Parallel() + + var events []*interpreter.CompositeValue + + inter, err := parseCheckAndInterpretWithOptions(t, ` + resource interface I { + access(all) let id: Int + event ResourceDestroyed(id: Int = self.id) + } + + resource A: I { + access(all) let id: Int + + init(id: Int) { + self.id = id + } + + event ResourceDestroyed(id: Int = self.id) + } + + resource B: I { + access(all) let id: Int + + init(id: Int) { + self.id = id + } + + event ResourceDestroyed(id: Int = self.id) + } + + fun test() { + let a <- create A(id: 1) + let b <- create B(id: 2) + let is: @[AnyResource] <- [<-a, <-b] + destroy is + } + `, ParseCheckAndInterpretOptions{ + Config: &interpreter.Config{ + OnEventEmitted: func(inter *interpreter.Interpreter, locationRange interpreter.LocationRange, event *interpreter.CompositeValue, eventType *sema.CompositeType) error { + events = append(events, event) + return nil + }, + }, + }) + + require.NoError(t, err) + _, err = inter.Invoke("test") + require.NoError(t, err) + + require.Len(t, events, 4) + require.Equal(t, events[0].QualifiedIdentifier, "I.ResourceDestroyed") + require.Equal(t, events[0].GetField(inter, interpreter.EmptyLocationRange, "id"), interpreter.NewIntValueFromInt64(nil, 1)) + require.Equal(t, events[1].QualifiedIdentifier, "A.ResourceDestroyed") + require.Equal(t, events[1].GetField(inter, interpreter.EmptyLocationRange, "id"), interpreter.NewIntValueFromInt64(nil, 1)) + require.Equal(t, events[2].QualifiedIdentifier, "I.ResourceDestroyed") + require.Equal(t, events[2].GetField(inter, interpreter.EmptyLocationRange, "id"), interpreter.NewIntValueFromInt64(nil, 2)) + require.Equal(t, events[3].QualifiedIdentifier, "B.ResourceDestroyed") + require.Equal(t, events[3].GetField(inter, interpreter.EmptyLocationRange, "id"), interpreter.NewIntValueFromInt64(nil, 2)) +} + +func TestInterpretResourceInterfaceDefaultDestroyEventMultipleInheritance(t *testing.T) { + + t.Parallel() + + var events []*interpreter.CompositeValue + + inter, err := parseCheckAndInterpretWithOptions(t, ` + resource interface I { + access(all) let id: Int + event ResourceDestroyed(id: Int = self.id) + } + + resource interface J { + access(all) let id: Int + event ResourceDestroyed(id: Int = self.id) + } + + resource A: I, J { + access(all) let id: Int + + init(id: Int) { + self.id = id + } + + event ResourceDestroyed(id: Int = self.id) + } + + fun test() { + let a <- create A(id: 1) + destroy a + } + `, ParseCheckAndInterpretOptions{ + Config: &interpreter.Config{ + OnEventEmitted: func(inter *interpreter.Interpreter, locationRange interpreter.LocationRange, event *interpreter.CompositeValue, eventType *sema.CompositeType) error { + events = append(events, event) + return nil + }, + }, + }) + + require.NoError(t, err) + _, err = inter.Invoke("test") + require.NoError(t, err) + + require.Len(t, events, 3) + require.Equal(t, events[0].QualifiedIdentifier, "I.ResourceDestroyed") + require.Equal(t, events[0].GetField(inter, interpreter.EmptyLocationRange, "id"), interpreter.NewIntValueFromInt64(nil, 1)) + require.Equal(t, events[1].QualifiedIdentifier, "J.ResourceDestroyed") + require.Equal(t, events[1].GetField(inter, interpreter.EmptyLocationRange, "id"), interpreter.NewIntValueFromInt64(nil, 1)) + require.Equal(t, events[2].QualifiedIdentifier, "A.ResourceDestroyed") + require.Equal(t, events[2].GetField(inter, interpreter.EmptyLocationRange, "id"), interpreter.NewIntValueFromInt64(nil, 1)) +} + +func TestInterpretResourceInterfaceDefaultDestroyEventIndirectInheritance(t *testing.T) { + + t.Parallel() + + var events []*interpreter.CompositeValue + + inter, err := parseCheckAndInterpretWithOptions(t, ` + resource interface I { + access(all) let id: Int + event ResourceDestroyed(id: Int = self.id) + } + + resource interface J: I { + access(all) let id: Int + event ResourceDestroyed(id: Int = self.id) + } + + resource A: J { + access(all) let id: Int + + init(id: Int) { + self.id = id + } + + event ResourceDestroyed(id: Int = self.id) + } + + fun test() { + let a <- create A(id: 1) + destroy a + } + `, ParseCheckAndInterpretOptions{ + Config: &interpreter.Config{ + OnEventEmitted: func(inter *interpreter.Interpreter, locationRange interpreter.LocationRange, event *interpreter.CompositeValue, eventType *sema.CompositeType) error { + events = append(events, event) + return nil + }, + }, + }) + + require.NoError(t, err) + _, err = inter.Invoke("test") + require.NoError(t, err) + + require.Len(t, events, 3) + require.Equal(t, events[0].QualifiedIdentifier, "J.ResourceDestroyed") + require.Equal(t, events[0].GetField(inter, interpreter.EmptyLocationRange, "id"), interpreter.NewIntValueFromInt64(nil, 1)) + require.Equal(t, events[1].QualifiedIdentifier, "I.ResourceDestroyed") + require.Equal(t, events[1].GetField(inter, interpreter.EmptyLocationRange, "id"), interpreter.NewIntValueFromInt64(nil, 1)) + require.Equal(t, events[2].QualifiedIdentifier, "A.ResourceDestroyed") + require.Equal(t, events[2].GetField(inter, interpreter.EmptyLocationRange, "id"), interpreter.NewIntValueFromInt64(nil, 1)) +} + +func TestInterpretResourceInterfaceDefaultDestroyEventNoCompositeEvent(t *testing.T) { + + t.Parallel() + + var events []*interpreter.CompositeValue + + inter, err := parseCheckAndInterpretWithOptions(t, ` + resource interface I { + access(all) let id: Int + event ResourceDestroyed(id: Int = self.id) + } + + resource interface J: I { + access(all) let id: Int + } + + resource A: J { + access(all) let id: Int + + init(id: Int) { + self.id = id + } + } + + fun test() { + let a <- create A(id: 1) + destroy a + } + `, ParseCheckAndInterpretOptions{ + Config: &interpreter.Config{ + OnEventEmitted: func(inter *interpreter.Interpreter, locationRange interpreter.LocationRange, event *interpreter.CompositeValue, eventType *sema.CompositeType) error { + events = append(events, event) + return nil + }, + }, + }) + + require.NoError(t, err) + _, err = inter.Invoke("test") + require.NoError(t, err) + + require.Len(t, events, 1) + require.Equal(t, events[0].QualifiedIdentifier, "I.ResourceDestroyed") + require.Equal(t, events[0].GetField(inter, interpreter.EmptyLocationRange, "id"), interpreter.NewIntValueFromInt64(nil, 1)) +} From 1e83b482b5a33a28df6009b77d1dec313ecc9450 Mon Sep 17 00:00:00 2001 From: Daniel Sainati Date: Wed, 27 Sep 2023 16:12:16 -0400 Subject: [PATCH 07/16] add test for same name interfaces --- runtime/runtime_test.go | 153 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/runtime/runtime_test.go b/runtime/runtime_test.go index e015f6544b..9bf6840024 100644 --- a/runtime/runtime_test.go +++ b/runtime/runtime_test.go @@ -3483,6 +3483,159 @@ func TestRuntimeStorageLoadedDestructionConcreteTypeWithAttachmentUnloadedContra require.Equal(t, events[4].String(), "A.0000000000000001.Test.R.ResourceDestroyed(foo: 6)") } +func TestRuntimeStorageLoadedDestructionConcreteTypeSameNamedInterface(t *testing.T) { + + t.Parallel() + + runtime := NewTestInterpreterRuntimeWithAttachments() + + addressValue := Address{ + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, + } + + interfaceContract1 := []byte(` + access(all) contract TestInterface1 { + access(all) resource interface I { + access(all) var foo: Int + access(all) event ResourceDestroyed(foo: Int = self.foo) + } + } + `) + + interfaceContract2 := []byte(` + access(all) contract TestInterface2 { + access(all) resource interface I { + access(all) var foo: Int + access(all) event ResourceDestroyed(foo: Int = self.foo) + } + } + `) + + contract := []byte(` + import TestInterface1 from 0x01 + import TestInterface2 from 0x01 + + access(all) contract Test { + + access(all) resource R: TestInterface1.I, TestInterface2.I { + access(all) var foo: Int + access(all) event ResourceDestroyed(foo: Int = self.foo) + init() { + self.foo = 0 + } + access(all) fun setFoo(_ arg: Int) { + self.foo = arg + } + } + + init() { + // store nested resource in account on deployment + let r <-create R() + r.setFoo(3) + self.account.storage.save(<-r, to: /storage/r) + } + } + `) + + tx := []byte(` + import Test from 0x01 + + transaction { + + prepare(acct: auth(Storage) &Account) { + let r <- acct.storage.load<@Test.R>(from: /storage/r)! + r.setFoo(6) + destroy r + } + } + `) + + deploy := DeploymentTransaction("Test", contract) + deployInterface1 := DeploymentTransaction("TestInterface1", interfaceContract1) + deployInterface2 := DeploymentTransaction("TestInterface2", interfaceContract2) + + accountCodes := map[Location][]byte{} + var events []cadence.Event + + runtimeInterface := &TestRuntimeInterface{ + OnGetCode: func(location Location) (bytes []byte, err error) { + return accountCodes[location], nil + }, + Storage: NewTestLedger(nil, nil), + OnGetSigningAccounts: func() ([]Address, error) { + return []Address{addressValue}, nil + }, + OnResolveLocation: NewSingleIdentifierLocationResolver(t), + OnGetAccountContractCode: func(location common.AddressLocation) (code []byte, err error) { + return accountCodes[location], nil + }, + OnUpdateAccountContractCode: func(location common.AddressLocation, code []byte) error { + accountCodes[location] = code + return nil + }, + OnCreateAccount: func(payer Address) (address Address, err error) { + return addressValue, nil + }, + OnEmitEvent: func(event cadence.Event) error { + events = append(events, event) + return nil + }, + } + + nextTransactionLocation := NewTransactionLocationGenerator() + + err := runtime.ExecuteTransaction( + Script{ + Source: deployInterface1, + }, + Context{ + Interface: runtimeInterface, + Location: nextTransactionLocation(), + }, + ) + require.NoError(t, err) + + err = runtime.ExecuteTransaction( + Script{ + Source: deployInterface2, + }, + Context{ + Interface: runtimeInterface, + Location: nextTransactionLocation(), + }, + ) + require.NoError(t, err) + + err = runtime.ExecuteTransaction( + Script{ + Source: deploy, + }, + Context{ + Interface: runtimeInterface, + Location: nextTransactionLocation(), + }, + ) + require.NoError(t, err) + + err = runtime.ExecuteTransaction( + Script{ + Source: tx, + }, + Context{ + Interface: runtimeInterface, + Location: nextTransactionLocation(), + }) + require.NoError(t, err) + + require.Len(t, events, 6) + require.Equal(t, events[0].EventType.ID(), "flow.AccountContractAdded") + require.Equal(t, events[1].EventType.ID(), "flow.AccountContractAdded") + require.Equal(t, events[2].EventType.ID(), "flow.AccountContractAdded") + require.Equal(t, events[3].String(), "A.0000000000000001.TestInterface1.I.ResourceDestroyed(foo: 6)") + require.Equal(t, events[4].String(), "A.0000000000000001.TestInterface2.I.ResourceDestroyed(foo: 6)") + require.Equal(t, events[5].String(), "A.0000000000000001.Test.R.ResourceDestroyed(foo: 6)") +} + func TestRuntimeStorageLoadedDestructionAnyResource(t *testing.T) { t.Parallel() From 3ba46dfcfde2aede946e8addc140c65bc2d8c649 Mon Sep 17 00:00:00 2001 From: Daniel Sainati Date: Tue, 31 Oct 2023 12:41:45 -0400 Subject: [PATCH 08/16] respond to review --- runtime/interpreter/interpreter.go | 18 ++++---- runtime/interpreter/value.go | 2 +- runtime/sema/check_composite_declaration.go | 1 + runtime/sema/check_interface_declaration.go | 3 ++ runtime/sema/elaboration.go | 18 ++++++++ runtime/tests/interpreter/attachments_test.go | 42 ++++++++++++++++--- 6 files changed, 66 insertions(+), 18 deletions(-) diff --git a/runtime/interpreter/interpreter.go b/runtime/interpreter/interpreter.go index c9fe52dcab..bb88c31f8b 100644 --- a/runtime/interpreter/interpreter.go +++ b/runtime/interpreter/interpreter.go @@ -2256,17 +2256,13 @@ func (interpreter *Interpreter) declareInterface( ) var defaultDestroyEventConstructor FunctionValue - for _, nestedCompositeDeclaration := range declaration.Members.Composites() { - // statically we know there is at most one of these - if nestedCompositeDeclaration.IsResourceDestructionDefaultEvent() { - var nestedVariable *Variable - lexicalScope, nestedVariable = interpreter.declareCompositeValue( - nestedCompositeDeclaration, - lexicalScope, - ) - defaultDestroyEventConstructor = nestedVariable.GetValue().(FunctionValue) - break - } + if defautlDestroyEvent := interpreter.Program.Elaboration.DefaultDestroyDeclaration(declaration); defautlDestroyEvent != nil { + var nestedVariable *Variable + lexicalScope, nestedVariable = interpreter.declareCompositeValue( + defautlDestroyEvent, + lexicalScope, + ) + defaultDestroyEventConstructor = nestedVariable.GetValue().(FunctionValue) } functionWrappers := interpreter.functionWrappers(declaration.Members, lexicalScope) diff --git a/runtime/interpreter/value.go b/runtime/interpreter/value.go index cf01c6eeed..3582b1112d 100644 --- a/runtime/interpreter/value.go +++ b/runtime/interpreter/value.go @@ -16380,7 +16380,7 @@ type CompositeField struct { } const unrepresentableNamePrefix = "$" -const resourceDefaultDestroyEventPrefix = "ResourceDestroyed" + unrepresentableNamePrefix +const resourceDefaultDestroyEventPrefix = ast.ResourceDestructionDefaultEventName + unrepresentableNamePrefix var _ TypeIndexableValue = &CompositeValue{} diff --git a/runtime/sema/check_composite_declaration.go b/runtime/sema/check_composite_declaration.go index 8cff51b91b..2b4ad8e0b3 100644 --- a/runtime/sema/check_composite_declaration.go +++ b/runtime/sema/check_composite_declaration.go @@ -845,6 +845,7 @@ func (checker *Checker) declareCompositeLikeMembersAndValue( if identifier.Identifier == ast.ResourceDestructionDefaultEventName { // Find the default event's type declaration + checker.Elaboration.SetDefaultDestroyDeclaration(declaration, nestedCompositeDeclaration) defaultEventType := checker.typeActivations.Find(identifier.Identifier) defaultEventComposite := defaultEventType.Type.(*CompositeType) diff --git a/runtime/sema/check_interface_declaration.go b/runtime/sema/check_interface_declaration.go index 508c9a1c00..a5a27c4bd4 100644 --- a/runtime/sema/check_interface_declaration.go +++ b/runtime/sema/check_interface_declaration.go @@ -443,6 +443,9 @@ func (checker *Checker) declareInterfaceMembersAndValue(declaration *ast.Interfa for _, nestedCompositeDeclaration := range declaration.Members.Composites() { if nestedCompositeDeclaration.Kind() == common.CompositeKindEvent { if nestedCompositeDeclaration.IsResourceDestructionDefaultEvent() { + + checker.Elaboration.SetDefaultDestroyDeclaration(declaration, nestedCompositeDeclaration) + // Find the value declaration nestedEvent := checker.typeActivations.Find(nestedCompositeDeclaration.Identifier.Identifier) diff --git a/runtime/sema/elaboration.go b/runtime/sema/elaboration.go index 8211be53b2..749a9bbbf2 100644 --- a/runtime/sema/elaboration.go +++ b/runtime/sema/elaboration.go @@ -148,6 +148,7 @@ type Elaboration struct { nestedResourceMoveExpressions map[ast.Expression]struct{} compositeNestedDeclarations map[ast.CompositeLikeDeclaration]map[string]ast.Declaration interfaceNestedDeclarations map[*ast.InterfaceDeclaration]map[string]ast.Declaration + defaultDestroyDeclarations map[ast.Declaration]ast.CompositeLikeDeclaration postConditionsRewrites map[*ast.Conditions]PostConditionsRewrite emitStatementEventTypes map[*ast.EmitStatement]*CompositeType compositeTypes map[TypeID]*CompositeType @@ -735,6 +736,23 @@ func (e *Elaboration) SetInterfaceNestedDeclarations( e.interfaceNestedDeclarations[declaration] = nestedDeclaration } +func (e *Elaboration) DefaultDestroyDeclaration(declaration ast.Declaration) ast.CompositeLikeDeclaration { + if e.defaultDestroyDeclarations == nil { + return nil + } + return e.defaultDestroyDeclarations[declaration] +} + +func (e *Elaboration) SetDefaultDestroyDeclaration( + declaration ast.Declaration, + eventDeclaration ast.CompositeLikeDeclaration, +) { + if e.defaultDestroyDeclarations == nil { + e.defaultDestroyDeclarations = map[ast.Declaration]ast.CompositeLikeDeclaration{} + } + e.defaultDestroyDeclarations[declaration] = eventDeclaration +} + func (e *Elaboration) PostConditionsRewrite(conditions *ast.Conditions) (rewrite PostConditionsRewrite) { if e.postConditionsRewrites == nil { return diff --git a/runtime/tests/interpreter/attachments_test.go b/runtime/tests/interpreter/attachments_test.go index 36c3c1ac70..9428d38f83 100644 --- a/runtime/tests/interpreter/attachments_test.go +++ b/runtime/tests/interpreter/attachments_test.go @@ -1256,7 +1256,12 @@ func TestInterpretAttachmentDestructor(t *testing.T) { } `, ParseCheckAndInterpretOptions{ Config: &interpreter.Config{ - OnEventEmitted: func(inter *interpreter.Interpreter, locationRange interpreter.LocationRange, event *interpreter.CompositeValue, eventType *sema.CompositeType) error { + OnEventEmitted: func( + _ *interpreter.Interpreter, + _ interpreter.LocationRange, + event *interpreter.CompositeValue, + _ *sema.CompositeType, + ) error { events = append(events, event) return nil }, @@ -1301,7 +1306,12 @@ func TestInterpretAttachmentDestructor(t *testing.T) { } `, ParseCheckAndInterpretOptions{ Config: &interpreter.Config{ - OnEventEmitted: func(inter *interpreter.Interpreter, locationRange interpreter.LocationRange, event *interpreter.CompositeValue, eventType *sema.CompositeType) error { + OnEventEmitted: func( + _ *interpreter.Interpreter, + _ interpreter.LocationRange, + event *interpreter.CompositeValue, + _ *sema.CompositeType, + ) error { events = append(events, event) return nil }, @@ -1342,7 +1352,12 @@ func TestInterpretAttachmentDestructor(t *testing.T) { } `, ParseCheckAndInterpretOptions{ Config: &interpreter.Config{ - OnEventEmitted: func(inter *interpreter.Interpreter, locationRange interpreter.LocationRange, event *interpreter.CompositeValue, eventType *sema.CompositeType) error { + OnEventEmitted: func( + _ *interpreter.Interpreter, + _ interpreter.LocationRange, + event *interpreter.CompositeValue, + _ *sema.CompositeType, + ) error { events = append(events, event) return nil }, @@ -1386,7 +1401,12 @@ func TestInterpretAttachmentDestructor(t *testing.T) { } `, ParseCheckAndInterpretOptions{ Config: &interpreter.Config{ - OnEventEmitted: func(inter *interpreter.Interpreter, locationRange interpreter.LocationRange, event *interpreter.CompositeValue, eventType *sema.CompositeType) error { + OnEventEmitted: func( + _ *interpreter.Interpreter, + _ interpreter.LocationRange, + event *interpreter.CompositeValue, + _ *sema.CompositeType, + ) error { events = append(events, event) return nil }, @@ -1434,7 +1454,12 @@ func TestInterpretAttachmentDestructor(t *testing.T) { } `, ParseCheckAndInterpretOptions{ Config: &interpreter.Config{ - OnEventEmitted: func(inter *interpreter.Interpreter, locationRange interpreter.LocationRange, event *interpreter.CompositeValue, eventType *sema.CompositeType) error { + OnEventEmitted: func( + _ *interpreter.Interpreter, + _ interpreter.LocationRange, + event *interpreter.CompositeValue, + _ *sema.CompositeType, + ) error { events = append(events, event) return nil }, @@ -1490,7 +1515,12 @@ func TestInterpretAttachmentDestructor(t *testing.T) { } `, ParseCheckAndInterpretOptions{ Config: &interpreter.Config{ - OnEventEmitted: func(inter *interpreter.Interpreter, locationRange interpreter.LocationRange, event *interpreter.CompositeValue, eventType *sema.CompositeType) error { + OnEventEmitted: func( + _ *interpreter.Interpreter, + _ interpreter.LocationRange, + event *interpreter.CompositeValue, + _ *sema.CompositeType, + ) error { events = append(events, event) return nil }, From 366366d6b067f58cdcc436f611f7183d0d42608e Mon Sep 17 00:00:00 2001 From: Daniel Sainati Date: Tue, 31 Oct 2023 13:46:00 -0400 Subject: [PATCH 09/16] use deterministic ordered function map --- runtime/environment.go | 2 +- runtime/interpreter/interpreter.go | 59 +++++++++++-------- runtime/interpreter/value.go | 22 +++++-- runtime/predeclaredvalues_test.go | 27 +++++---- runtime/stdlib/builtin.go | 4 +- runtime/stdlib/publickey.go | 11 ++-- runtime/stdlib/test_contract.go | 28 ++++----- runtime/tests/interpreter/import_test.go | 10 ++-- runtime/tests/interpreter/interpreter_test.go | 3 +- 9 files changed, 95 insertions(+), 71 deletions(-) diff --git a/runtime/environment.go b/runtime/environment.go index 9c1e40bb7c..895d6f44e5 100644 --- a/runtime/environment.go +++ b/runtime/environment.go @@ -981,7 +981,7 @@ func (e *interpreterEnvironment) newCompositeValueFunctionsHandler() interpreter inter *interpreter.Interpreter, locationRange interpreter.LocationRange, compositeValue *interpreter.CompositeValue, - ) map[string]interpreter.FunctionValue { + ) *interpreter.FunctionOrderedMap { handler := e.compositeValueFunctionsHandlers[compositeValue.TypeID()] if handler == nil { diff --git a/runtime/interpreter/interpreter.go b/runtime/interpreter/interpreter.go index bb88c31f8b..ee24b07fdc 100644 --- a/runtime/interpreter/interpreter.go +++ b/runtime/interpreter/interpreter.go @@ -37,6 +37,7 @@ import ( "github.com/onflow/cadence/runtime/activations" "github.com/onflow/cadence/runtime/ast" "github.com/onflow/cadence/runtime/common" + "github.com/onflow/cadence/runtime/common/orderedmap" "github.com/onflow/cadence/runtime/errors" "github.com/onflow/cadence/runtime/sema" ) @@ -165,7 +166,7 @@ type CompositeValueFunctionsHandlerFunc func( inter *Interpreter, locationRange LocationRange, compositeValue *CompositeValue, -) map[string]FunctionValue +) *FunctionOrderedMap // CompositeTypeCode contains the "prepared" / "callable" "code" // for the functions and the destructor of a composite @@ -174,7 +175,7 @@ type CompositeValueFunctionsHandlerFunc func( // As there is no support for inheritance of concrete types, // these are the "leaf" nodes in the call chain, and are functions. type CompositeTypeCode struct { - CompositeFunctions map[string]FunctionValue + CompositeFunctions *FunctionOrderedMap } type FunctionWrapper = func(inner FunctionValue) FunctionValue @@ -187,7 +188,7 @@ type FunctionWrapper = func(inner FunctionValue) FunctionValue type WrapperCode struct { InitializerFunctionWrapper FunctionWrapper FunctionWrappers map[string]FunctionWrapper - Functions map[string]FunctionValue + Functions *FunctionOrderedMap DefaultDestroyEventConstructor FunctionValue } @@ -1195,7 +1196,7 @@ func (interpreter *Interpreter) declareNonEnumCompositeValue( functions := interpreter.compositeFunctions(declaration, lexicalScope) if destroyEventConstructor != nil { - functions[resourceDefaultDestroyEventName(compositeType)] = destroyEventConstructor + functions.Set(resourceDefaultDestroyEventName(compositeType), destroyEventConstructor) } wrapFunctions := func(ty *sema.InterfaceType, code WrapperCode) { @@ -1215,14 +1216,16 @@ func (interpreter *Interpreter) declareNonEnumCompositeValue( // we only apply the function wrapper to each function, // the order does not matter. - for name, function := range code.Functions { //nolint:maprange - if functions[name] != nil { - continue - } - if functions == nil { - functions = map[string]FunctionValue{} - } - functions[name] = function + if code.Functions != nil { + code.Functions.Foreach(func(name string, function FunctionValue) { + if functions == nil { + functions = orderedmap.New[FunctionOrderedMap](code.Functions.Len()) + } + if functions.Contains(name) { + return + } + functions.Set(name, function) + }) } // Wrap functions @@ -1232,11 +1235,12 @@ func (interpreter *Interpreter) declareNonEnumCompositeValue( // the order does not matter. for name, functionWrapper := range code.FunctionWrappers { //nolint:maprange - functions[name] = functionWrapper(functions[name]) + fn, _ := functions.Get(name) + functions.Set(name, functionWrapper(fn)) } if code.DefaultDestroyEventConstructor != nil { - functions[resourceDefaultDestroyEventName(ty)] = code.DefaultDestroyEventConstructor + functions.Set(resourceDefaultDestroyEventName(ty), code.DefaultDestroyEventConstructor) } } @@ -1594,7 +1598,7 @@ func (interpreter *Interpreter) compositeInitializerFunction( func (interpreter *Interpreter) defaultFunctions( members *ast.Members, lexicalScope *VariableActivation, -) map[string]FunctionValue { +) *FunctionOrderedMap { functionDeclarations := members.Functions() functionCount := len(functionDeclarations) @@ -1603,7 +1607,7 @@ func (interpreter *Interpreter) defaultFunctions( return nil } - functions := make(map[string]FunctionValue, functionCount) + functions := orderedmap.New[FunctionOrderedMap](functionCount) for _, functionDeclaration := range functionDeclarations { name := functionDeclaration.Identifier.Identifier @@ -1611,9 +1615,12 @@ func (interpreter *Interpreter) defaultFunctions( continue } - functions[name] = interpreter.compositeFunction( - functionDeclaration, - lexicalScope, + functions.Set( + name, + interpreter.compositeFunction( + functionDeclaration, + lexicalScope, + ), ) } @@ -1623,17 +1630,19 @@ func (interpreter *Interpreter) defaultFunctions( func (interpreter *Interpreter) compositeFunctions( compositeDeclaration ast.CompositeLikeDeclaration, lexicalScope *VariableActivation, -) map[string]FunctionValue { +) *FunctionOrderedMap { - functions := map[string]FunctionValue{} + functions := orderedmap.New[FunctionOrderedMap](len(compositeDeclaration.DeclarationMembers().Functions())) for _, functionDeclaration := range compositeDeclaration.DeclarationMembers().Functions() { name := functionDeclaration.Identifier.Identifier - functions[name] = + functions.Set( + name, interpreter.compositeFunction( functionDeclaration, lexicalScope, - ) + ), + ) } return functions @@ -4615,9 +4624,9 @@ func (interpreter *Interpreter) GetCompositeValueInjectedFields(v *CompositeValu func (interpreter *Interpreter) GetCompositeValueFunctions( v *CompositeValue, locationRange LocationRange, -) map[string]FunctionValue { +) *FunctionOrderedMap { - var functions map[string]FunctionValue + var functions *FunctionOrderedMap typeID := v.TypeID() diff --git a/runtime/interpreter/value.go b/runtime/interpreter/value.go index 3582b1112d..1ef31575a8 100644 --- a/runtime/interpreter/value.go +++ b/runtime/interpreter/value.go @@ -37,6 +37,7 @@ import ( "github.com/onflow/cadence/runtime/ast" "github.com/onflow/cadence/runtime/common" + "github.com/onflow/cadence/runtime/common/orderedmap" "github.com/onflow/cadence/runtime/errors" "github.com/onflow/cadence/runtime/format" "github.com/onflow/cadence/runtime/sema" @@ -16350,6 +16351,8 @@ func (UFix64Value) Scale() int { // CompositeValue +type FunctionOrderedMap = orderedmap.OrderedMap[string, FunctionValue] + type CompositeValue struct { Location common.Location staticType StaticType @@ -16357,7 +16360,7 @@ type CompositeValue struct { injectedFields map[string]Value computedFields map[string]ComputedField NestedVariables map[string]*Variable - Functions map[string]FunctionValue + Functions *FunctionOrderedMap dictionary *atree.OrderedMap typeID TypeID @@ -16591,11 +16594,14 @@ func resourceDefaultDestroyEventName(t sema.ContainerType) string { } func (v *CompositeValue) defaultDestroyEventConstructors() (constructors []FunctionValue) { - for name, f := range v.Functions { + if v.Functions == nil { + return + } + v.Functions.Foreach(func(name string, f FunctionValue) { if strings.HasPrefix(name, resourceDefaultDestroyEventPrefix) { constructors = append(constructors, f) } - } + }) return } @@ -16831,9 +16837,13 @@ func (v *CompositeValue) GetFunction(interpreter *Interpreter, locationRange Loc if v.Functions == nil { v.Functions = interpreter.GetCompositeValueFunctions(v, locationRange) } + // if no functions were produced, the `Get` below will be nil + if v.Functions == nil { + return nil + } - function, ok := v.Functions[name] - if !ok { + function, present := v.Functions.Get(name) + if !present { return nil } @@ -17718,7 +17728,7 @@ func NewEnumCaseValue( locationRange LocationRange, enumType *sema.CompositeType, rawValue NumberValue, - functions map[string]FunctionValue, + functions *FunctionOrderedMap, ) *CompositeValue { fields := []CompositeField{ diff --git a/runtime/predeclaredvalues_test.go b/runtime/predeclaredvalues_test.go index b2f4e6cbbb..62b7633440 100644 --- a/runtime/predeclaredvalues_test.go +++ b/runtime/predeclaredvalues_test.go @@ -29,6 +29,7 @@ import ( "github.com/onflow/cadence" . "github.com/onflow/cadence/runtime" "github.com/onflow/cadence/runtime/common" + "github.com/onflow/cadence/runtime/common/orderedmap" "github.com/onflow/cadence/runtime/interpreter" "github.com/onflow/cadence/runtime/sema" "github.com/onflow/cadence/runtime/stdlib" @@ -697,21 +698,21 @@ func TestRuntimePredeclaredTypeWithInjectedFunctions(t *testing.T) { inter *interpreter.Interpreter, locationRange interpreter.LocationRange, compositeValue *interpreter.CompositeValue, - ) map[string]interpreter.FunctionValue { + ) *interpreter.FunctionOrderedMap { require.NotNil(t, compositeValue) - return map[string]interpreter.FunctionValue{ - fooFunctionName: interpreter.NewHostFunctionValue( - inter, - fooFunctionType, - func(invocation interpreter.Invocation) interpreter.Value { - arg := invocation.Arguments[0] - require.IsType(t, interpreter.UInt8Value(0), arg) - - return interpreter.NewUnmeteredStringValue(strconv.Itoa(int(arg.(interpreter.UInt8Value) + 1))) - }, - ), - } + functions := orderedmap.New[interpreter.FunctionOrderedMap](1) + functions.Set(fooFunctionName, interpreter.NewHostFunctionValue( + inter, + fooFunctionType, + func(invocation interpreter.Invocation) interpreter.Value { + arg := invocation.Arguments[0] + require.IsType(t, interpreter.UInt8Value(0), arg) + + return interpreter.NewUnmeteredStringValue(strconv.Itoa(int(arg.(interpreter.UInt8Value) + 1))) + }, + )) + return functions }, ) diff --git a/runtime/stdlib/builtin.go b/runtime/stdlib/builtin.go index e1f085899f..65d71d2274 100644 --- a/runtime/stdlib/builtin.go +++ b/runtime/stdlib/builtin.go @@ -67,7 +67,7 @@ type CompositeValueFunctionsHandler func( inter *interpreter.Interpreter, locationRange interpreter.LocationRange, compositeValue *interpreter.CompositeValue, -) map[string]interpreter.FunctionValue +) *interpreter.FunctionOrderedMap type CompositeValueFunctionsHandlers map[common.TypeID]CompositeValueFunctionsHandler @@ -79,7 +79,7 @@ func DefaultStandardLibraryCompositeValueFunctionHandlers( inter *interpreter.Interpreter, _ interpreter.LocationRange, _ *interpreter.CompositeValue, - ) map[string]interpreter.FunctionValue { + ) *interpreter.FunctionOrderedMap { return PublicKeyFunctions(inter, handler) }, } diff --git a/runtime/stdlib/publickey.go b/runtime/stdlib/publickey.go index 854939269f..069fa27989 100644 --- a/runtime/stdlib/publickey.go +++ b/runtime/stdlib/publickey.go @@ -20,6 +20,7 @@ package stdlib import ( "github.com/onflow/cadence/runtime/common" + "github.com/onflow/cadence/runtime/common/orderedmap" "github.com/onflow/cadence/runtime/errors" "github.com/onflow/cadence/runtime/interpreter" "github.com/onflow/cadence/runtime/sema" @@ -350,9 +351,9 @@ type PublicKeyFunctionsHandler interface { func PublicKeyFunctions( gauge common.MemoryGauge, handler PublicKeyFunctionsHandler, -) map[string]interpreter.FunctionValue { - return map[string]interpreter.FunctionValue{ - sema.PublicKeyTypeVerifyFunctionName: newPublicKeyVerifySignatureFunction(gauge, handler), - sema.PublicKeyTypeVerifyPoPFunctionName: newPublicKeyVerifyPoPFunction(gauge, handler), - } +) *interpreter.FunctionOrderedMap { + functions := orderedmap.New[interpreter.FunctionOrderedMap](2) + functions.Set(sema.PublicKeyTypeVerifyFunctionName, newPublicKeyVerifySignatureFunction(gauge, handler)) + functions.Set(sema.PublicKeyTypeVerifyPoPFunctionName, newPublicKeyVerifyPoPFunction(gauge, handler)) + return functions } diff --git a/runtime/stdlib/test_contract.go b/runtime/stdlib/test_contract.go index fb0737b675..862f234ac1 100644 --- a/runtime/stdlib/test_contract.go +++ b/runtime/stdlib/test_contract.go @@ -1230,22 +1230,22 @@ func (t *TestContractType) NewTestContract( compositeValue := value.(*interpreter.CompositeValue) // Inject natively implemented function values - compositeValue.Functions[testTypeAssertFunctionName] = testTypeAssertFunction - compositeValue.Functions[testTypeAssertEqualFunctionName] = testTypeAssertEqualFunction - compositeValue.Functions[testTypeFailFunctionName] = testTypeFailFunction - compositeValue.Functions[testTypeExpectFunctionName] = t.expectFunction - compositeValue.Functions[testTypeReadFileFunctionName] = - newTestTypeReadFileFunction(testFramework) + compositeValue.Functions.Set(testTypeAssertFunctionName, testTypeAssertFunction) + compositeValue.Functions.Set(testTypeAssertEqualFunctionName, testTypeAssertEqualFunction) + compositeValue.Functions.Set(testTypeFailFunctionName, testTypeFailFunction) + compositeValue.Functions.Set(testTypeExpectFunctionName, t.expectFunction) + compositeValue.Functions.Set(testTypeReadFileFunctionName, + newTestTypeReadFileFunction(testFramework)) // Inject natively implemented matchers - compositeValue.Functions[testTypeNewMatcherFunctionName] = t.newMatcherFunction - compositeValue.Functions[testTypeEqualFunctionName] = t.equalFunction - compositeValue.Functions[testTypeBeEmptyFunctionName] = t.beEmptyFunction - compositeValue.Functions[testTypeHaveElementCountFunctionName] = t.haveElementCountFunction - compositeValue.Functions[testTypeContainFunctionName] = t.containFunction - compositeValue.Functions[testTypeBeGreaterThanFunctionName] = t.beGreaterThanFunction - compositeValue.Functions[testTypeBeLessThanFunctionName] = t.beLessThanFunction - compositeValue.Functions[testExpectFailureFunctionName] = t.expectFailureFunction + compositeValue.Functions.Set(testTypeNewMatcherFunctionName, t.newMatcherFunction) + compositeValue.Functions.Set(testTypeEqualFunctionName, t.equalFunction) + compositeValue.Functions.Set(testTypeBeEmptyFunctionName, t.beEmptyFunction) + compositeValue.Functions.Set(testTypeHaveElementCountFunctionName, t.haveElementCountFunction) + compositeValue.Functions.Set(testTypeContainFunctionName, t.containFunction) + compositeValue.Functions.Set(testTypeBeGreaterThanFunctionName, t.beGreaterThanFunction) + compositeValue.Functions.Set(testTypeBeLessThanFunctionName, t.beLessThanFunction) + compositeValue.Functions.Set(testExpectFailureFunctionName, t.expectFailureFunction) return compositeValue, nil } diff --git a/runtime/tests/interpreter/import_test.go b/runtime/tests/interpreter/import_test.go index ff699b8cbc..4eaec78726 100644 --- a/runtime/tests/interpreter/import_test.go +++ b/runtime/tests/interpreter/import_test.go @@ -24,6 +24,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/onflow/cadence/runtime/common/orderedmap" . "github.com/onflow/cadence/runtime/tests/utils" "github.com/onflow/cadence/runtime/ast" @@ -91,9 +92,10 @@ func TestInterpretVirtualImport(t *testing.T) { nil, common.ZeroAddress, ) - - value.Functions = map[string]interpreter.FunctionValue{ - "bar": interpreter.NewHostFunctionValue( + value.Functions = orderedmap.New[interpreter.FunctionOrderedMap](1) + value.Functions.Set( + "bar", + interpreter.NewHostFunctionValue( inter, &sema.FunctionType{ ReturnTypeAnnotation: sema.UIntTypeAnnotation, @@ -102,7 +104,7 @@ func TestInterpretVirtualImport(t *testing.T) { return interpreter.NewUnmeteredUInt64Value(42) }, ), - } + ) elaboration := sema.NewElaboration(nil) elaboration.SetCompositeType( diff --git a/runtime/tests/interpreter/interpreter_test.go b/runtime/tests/interpreter/interpreter_test.go index 8c38cd68c2..1af9fb781d 100644 --- a/runtime/tests/interpreter/interpreter_test.go +++ b/runtime/tests/interpreter/interpreter_test.go @@ -28,6 +28,7 @@ import ( "github.com/onflow/cadence/runtime" "github.com/onflow/cadence/runtime/activations" + "github.com/onflow/cadence/runtime/common/orderedmap" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -7383,7 +7384,7 @@ func TestInterpretEmitEventParameterTypes(t *testing.T) { nil, common.ZeroAddress, ) - sValue.Functions = map[string]interpreter.FunctionValue{} + sValue.Functions = orderedmap.New[interpreter.FunctionOrderedMap](0) validTypes := map[string]testValue{ "String": { From 229a189980f5701d78ba8efb4019e0d4da3ac33f Mon Sep 17 00:00:00 2001 From: Daniel Sainati Date: Wed, 1 Nov 2023 14:41:24 -0400 Subject: [PATCH 10/16] Update runtime/interpreter/interpreter.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bastian Müller --- runtime/interpreter/interpreter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/interpreter/interpreter.go b/runtime/interpreter/interpreter.go index ee24b07fdc..eff3b28ea9 100644 --- a/runtime/interpreter/interpreter.go +++ b/runtime/interpreter/interpreter.go @@ -980,7 +980,7 @@ func (interpreter *Interpreter) declareAttachmentValue( return interpreter.declareCompositeValue(declaration, lexicalScope) } -// evaluates all the implicit default arguments to the default destroy event +// evaluateDefaultDestroyEvent evaluates all the implicit default arguments to the default destroy event. // // the handling of default arguments makes a number of assumptions to simplify the implementation; // namely that a) all default arguments are lazily evaluated at the site of the invocation, From e0b9d0378d365165ec2848458c9890ee01313557 Mon Sep 17 00:00:00 2001 From: Daniel Sainati Date: Wed, 1 Nov 2023 15:09:23 -0400 Subject: [PATCH 11/16] respond to review --- runtime/interpreter/interpreter.go | 31 +++++++++++++++--------------- runtime/interpreter/value.go | 8 ++++++-- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/runtime/interpreter/interpreter.go b/runtime/interpreter/interpreter.go index eff3b28ea9..d229a9b0ce 100644 --- a/runtime/interpreter/interpreter.go +++ b/runtime/interpreter/interpreter.go @@ -989,31 +989,30 @@ func (interpreter *Interpreter) declareAttachmentValue( // // if we plan to generalize this further, we will need to relax those assumptions func (interpreter *Interpreter) evaluateDefaultDestroyEvent( - containerComposite *CompositeValue, - compositeDecl *ast.CompositeDeclaration, - compositeType *sema.CompositeType, - locationRange LocationRange, + containingResourceComposite *CompositeValue, + eventDecl *ast.CompositeDeclaration, ) (arguments []Value) { - parameters := compositeDecl.DeclarationMembers().Initializers()[0].FunctionDeclaration.ParameterList.Parameters + parameters := eventDecl.DeclarationMembers().Initializers()[0].FunctionDeclaration.ParameterList.Parameters - interpreter.activations.PushNewWithParent(interpreter.activations.CurrentOrNew()) + defer func() { + // Only unwind the call stack if there was no error + if r := recover(); r != nil { + panic(r) + } + interpreter.SharedState.callStack.Pop() + }() defer interpreter.activations.Pop() - var self MemberAccessibleValue = containerComposite - if containerComposite.Kind == common.CompositeKindAttachment { + var self MemberAccessibleValue = containingResourceComposite + if containingResourceComposite.Kind == common.CompositeKindAttachment { var base *EphemeralReferenceValue - base, self = attachmentBaseAndSelfValues(interpreter, containerComposite) + base, self = attachmentBaseAndSelfValues(interpreter, containingResourceComposite) interpreter.declareVariable(sema.BaseIdentifier, base) } interpreter.declareVariable(sema.SelfIdentifier, self) for _, parameter := range parameters { // lazily evaluate the default argument expressions - // note that we must evaluate them in the interpreter context that existed when the event - // was defined (i.e. the contract defining the resource) rather than the interpreter context - // that exists when the resource is destroyed. We accomplish this by using the original interpreter of the - // composite declaration, rather than the interpreter of the destroy expression - defaultArg := interpreter.evalExpression(parameter.DefaultArgument) arguments = append(arguments, defaultArg) } @@ -1166,11 +1165,11 @@ func (interpreter *Interpreter) declareNonEnumCompositeValue( if compositeDecl.IsResourceDestructionDefaultEvent() { // we implicitly pass the containing composite value as an argument to this invocation containerComposite := invocation.Arguments[0].(*CompositeValue) + interpreter.activations.PushNewWithParent(inter.activations.CurrentOrNew()) + interpreter.SharedState.callStack.Push(invocation) invocation.Arguments = interpreter.evaluateDefaultDestroyEvent( containerComposite, compositeDecl, - compositeType, - locationRange, ) } diff --git a/runtime/interpreter/value.go b/runtime/interpreter/value.go index 1ef31575a8..cc03e7e0e2 100644 --- a/runtime/interpreter/value.go +++ b/runtime/interpreter/value.go @@ -16593,6 +16593,10 @@ func resourceDefaultDestroyEventName(t sema.ContainerType) string { return resourceDefaultDestroyEventPrefix + string(t.ID()) } +// get all the default destroy event constructors associated with this composite value. +// note that there can be more than one in the case where a resource inherits from an interface +// that also defines a default destroy event. When that composite is destroyed, all of these +// events will need to be emitted. func (v *CompositeValue) defaultDestroyEventConstructors() (constructors []FunctionValue) { if v.Functions == nil { return @@ -16648,7 +16652,7 @@ func (v *CompositeValue) Destroy(interpreter *Interpreter, locationRange Locatio // pass the container value to the creation of the default event as an implicit argument, so that // its fields are accessible in the body of the event constructor - mockInvocation := NewInvocation( + eventConstructorInvocation := NewInvocation( interpreter, nil, nil, @@ -16659,7 +16663,7 @@ func (v *CompositeValue) Destroy(interpreter *Interpreter, locationRange Locatio locationRange, ) - event := constructor.invoke(mockInvocation).(*CompositeValue) + event := constructor.invoke(eventConstructorInvocation).(*CompositeValue) eventType := interpreter.MustSemaTypeOfValue(event).(*sema.CompositeType) // emit the event once destruction is complete From 3cd0f5d1b638f539394cb9ef37d967967077f487 Mon Sep 17 00:00:00 2001 From: Daniel Sainati Date: Wed, 1 Nov 2023 16:35:24 -0400 Subject: [PATCH 12/16] add comment --- runtime/interpreter/interpreter.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/runtime/interpreter/interpreter.go b/runtime/interpreter/interpreter.go index d229a9b0ce..d8aff83c0b 100644 --- a/runtime/interpreter/interpreter.go +++ b/runtime/interpreter/interpreter.go @@ -1012,7 +1012,12 @@ func (interpreter *Interpreter) evaluateDefaultDestroyEvent( interpreter.declareVariable(sema.SelfIdentifier, self) for _, parameter := range parameters { - // lazily evaluate the default argument expressions + // "lazily" evaluate the default argument expressions. + // This "lazy" with respect to the event's declaration: + // if we declare a default event `ResourceDestroyed(foo: Int = self.x)`, + // `self.x` is evaluated in the context that exists when the event is destroyed, + // not the context when it is declared. This function is only called after the destroy + // triggers the event emission, so with respect to this function it's "eager". defaultArg := interpreter.evalExpression(parameter.DefaultArgument) arguments = append(arguments, defaultArg) } From e5af208fb0d206bada01d64718161f064baaf73d Mon Sep 17 00:00:00 2001 From: Daniel Sainati Date: Wed, 1 Nov 2023 17:14:24 -0400 Subject: [PATCH 13/16] rename variables --- runtime/interpreter/interpreter.go | 69 ++++++++++++++++-------------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/runtime/interpreter/interpreter.go b/runtime/interpreter/interpreter.go index d8aff83c0b..4ee5689bd0 100644 --- a/runtime/interpreter/interpreter.go +++ b/runtime/interpreter/interpreter.go @@ -988,28 +988,35 @@ func (interpreter *Interpreter) declareAttachmentValue( // and c) functions cannot currently be explicitly invoked if they have default arguments // // if we plan to generalize this further, we will need to relax those assumptions -func (interpreter *Interpreter) evaluateDefaultDestroyEvent( +func (declarationInterpreter *Interpreter) evaluateDefaultDestroyEvent( containingResourceComposite *CompositeValue, eventDecl *ast.CompositeDeclaration, + invocation Invocation, + invocationActivation *VariableActivation, ) (arguments []Value) { parameters := eventDecl.DeclarationMembers().Initializers()[0].FunctionDeclaration.ParameterList.Parameters + declarationInterpreter.activations.PushNewWithParent(invocationActivation) + declarationInterpreter.SharedState.callStack.Push(invocation) + + // interpreter.activations.PushNewWithParent(inter.activations.CurrentOrNew()) + // interpreter.SharedState.callStack.Push(invocation) defer func() { // Only unwind the call stack if there was no error if r := recover(); r != nil { panic(r) } - interpreter.SharedState.callStack.Pop() + declarationInterpreter.SharedState.callStack.Pop() }() - defer interpreter.activations.Pop() + defer declarationInterpreter.activations.Pop() var self MemberAccessibleValue = containingResourceComposite if containingResourceComposite.Kind == common.CompositeKindAttachment { var base *EphemeralReferenceValue - base, self = attachmentBaseAndSelfValues(interpreter, containingResourceComposite) - interpreter.declareVariable(sema.BaseIdentifier, base) + base, self = attachmentBaseAndSelfValues(declarationInterpreter, containingResourceComposite) + declarationInterpreter.declareVariable(sema.BaseIdentifier, base) } - interpreter.declareVariable(sema.SelfIdentifier, self) + declarationInterpreter.declareVariable(sema.SelfIdentifier, self) for _, parameter := range parameters { // "lazily" evaluate the default argument expressions. @@ -1018,7 +1025,7 @@ func (interpreter *Interpreter) evaluateDefaultDestroyEvent( // `self.x` is evaluated in the context that exists when the event is destroyed, // not the context when it is declared. This function is only called after the destroy // triggers the event emission, so with respect to this function it's "eager". - defaultArg := interpreter.evalExpression(parameter.DefaultArgument) + defaultArg := declarationInterpreter.evalExpression(parameter.DefaultArgument) arguments = append(arguments, defaultArg) } @@ -1055,7 +1062,7 @@ func (interpreter *Interpreter) declareCompositeValue( } } -func (interpreter *Interpreter) declareNonEnumCompositeValue( +func (declarationInterpreter *Interpreter) declareNonEnumCompositeValue( declaration ast.CompositeLikeDeclaration, lexicalScope *VariableActivation, ) ( @@ -1064,7 +1071,7 @@ func (interpreter *Interpreter) declareNonEnumCompositeValue( ) { identifier := declaration.DeclarationIdentifier().Identifier // NOTE: find *or* declare, as the function might have not been pre-declared (e.g. in the REPL) - variable = interpreter.findOrDeclareVariable(identifier) + variable = declarationInterpreter.findOrDeclareVariable(identifier) // Make the value available in the initializer lexicalScope.Set(identifier, variable) @@ -1077,15 +1084,15 @@ func (interpreter *Interpreter) declareNonEnumCompositeValue( var destroyEventConstructor FunctionValue (func() { - interpreter.activations.PushNewWithCurrent() - defer interpreter.activations.Pop() + declarationInterpreter.activations.PushNewWithCurrent() + defer declarationInterpreter.activations.Pop() // Pre-declare empty variables for all interfaces, composites, and function declarations predeclare := func(identifier ast.Identifier) { name := identifier.Identifier lexicalScope.Set( name, - interpreter.declareVariable(name, nil), + declarationInterpreter.declareVariable(name, nil), ) } @@ -1104,7 +1111,7 @@ func (interpreter *Interpreter) declareNonEnumCompositeValue( } for _, nestedInterfaceDeclaration := range members.Interfaces() { - interpreter.declareInterface(nestedInterfaceDeclaration, lexicalScope) + declarationInterpreter.declareInterface(nestedInterfaceDeclaration, lexicalScope) } for _, nestedCompositeDeclaration := range members.Composites() { @@ -1115,7 +1122,7 @@ func (interpreter *Interpreter) declareNonEnumCompositeValue( var nestedVariable *Variable lexicalScope, nestedVariable = - interpreter.declareCompositeValue( + declarationInterpreter.declareCompositeValue( nestedCompositeDeclaration, lexicalScope, ) @@ -1137,7 +1144,7 @@ func (interpreter *Interpreter) declareNonEnumCompositeValue( var nestedVariable *Variable lexicalScope, nestedVariable = - interpreter.declareAttachmentValue( + declarationInterpreter.declareAttachmentValue( nestedAttachmentDeclaration, lexicalScope, ) @@ -1147,17 +1154,17 @@ func (interpreter *Interpreter) declareNonEnumCompositeValue( } })() - compositeType := interpreter.Program.Elaboration.CompositeDeclarationType(declaration) + compositeType := declarationInterpreter.Program.Elaboration.CompositeDeclarationType(declaration) initializerType := compositeType.InitializerFunctionType() var initializerFunction FunctionValue if declaration.Kind() == common.CompositeKindEvent { initializerFunction = NewHostFunctionValue( - interpreter, + declarationInterpreter, initializerType, func(invocation Invocation) Value { - inter := invocation.Interpreter + invocationInterpreter := invocation.Interpreter locationRange := invocation.LocationRange self := *invocation.Self @@ -1170,18 +1177,18 @@ func (interpreter *Interpreter) declareNonEnumCompositeValue( if compositeDecl.IsResourceDestructionDefaultEvent() { // we implicitly pass the containing composite value as an argument to this invocation containerComposite := invocation.Arguments[0].(*CompositeValue) - interpreter.activations.PushNewWithParent(inter.activations.CurrentOrNew()) - interpreter.SharedState.callStack.Push(invocation) - invocation.Arguments = interpreter.evaluateDefaultDestroyEvent( + invocation.Arguments = declarationInterpreter.evaluateDefaultDestroyEvent( containerComposite, compositeDecl, + invocation, + invocationInterpreter.activations.CurrentOrNew(), ) } for i, argument := range invocation.Arguments { parameter := compositeType.ConstructorParameters[i] self.SetMember( - inter, + invocationInterpreter, locationRange, parameter.Identifier, argument, @@ -1191,13 +1198,13 @@ func (interpreter *Interpreter) declareNonEnumCompositeValue( }, ) } else { - compositeInitializerFunction := interpreter.compositeInitializerFunction(declaration, lexicalScope) + compositeInitializerFunction := declarationInterpreter.compositeInitializerFunction(declaration, lexicalScope) if compositeInitializerFunction != nil { initializerFunction = compositeInitializerFunction } } - functions := interpreter.compositeFunctions(declaration, lexicalScope) + functions := declarationInterpreter.compositeFunctions(declaration, lexicalScope) if destroyEventConstructor != nil { functions.Set(resourceDefaultDestroyEventName(compositeType), destroyEventConstructor) @@ -1251,24 +1258,24 @@ func (interpreter *Interpreter) declareNonEnumCompositeValue( conformances := compositeType.EffectiveInterfaceConformances() for i := len(conformances) - 1; i >= 0; i-- { conformance := conformances[i].InterfaceType - wrapFunctions(conformance, interpreter.SharedState.typeCodes.InterfaceCodes[conformance.ID()]) + wrapFunctions(conformance, declarationInterpreter.SharedState.typeCodes.InterfaceCodes[conformance.ID()]) } - interpreter.SharedState.typeCodes.CompositeCodes[compositeType.ID()] = CompositeTypeCode{ + declarationInterpreter.SharedState.typeCodes.CompositeCodes[compositeType.ID()] = CompositeTypeCode{ CompositeFunctions: functions, } - location := interpreter.Location + location := declarationInterpreter.Location qualifiedIdentifier := compositeType.QualifiedIdentifier() - config := interpreter.SharedState.Config + config := declarationInterpreter.SharedState.Config constructorType := compositeType.ConstructorFunctionType() constructorGenerator := func(address common.Address) *HostFunctionValue { return NewHostFunctionValue( - interpreter, + declarationInterpreter, constructorType, func(invocation Invocation) Value { @@ -1395,10 +1402,10 @@ func (interpreter *Interpreter) declareNonEnumCompositeValue( if declaration.Kind() == common.CompositeKindContract { variable.getter = func() Value { - positioned := ast.NewRangeFromPositioned(interpreter, declaration.DeclarationIdentifier()) + positioned := ast.NewRangeFromPositioned(declarationInterpreter, declaration.DeclarationIdentifier()) contractValue := config.ContractValueHandler( - interpreter, + declarationInterpreter, compositeType, constructorGenerator, positioned, From d8abdd4d778f2b6dfa4ddd1562590035e99891a3 Mon Sep 17 00:00:00 2001 From: Daniel Sainati Date: Wed, 1 Nov 2023 17:38:09 -0400 Subject: [PATCH 14/16] proper lexical scoping and testing --- runtime/interpreter/interpreter.go | 23 ++++------- runtime/tests/interpreter/resources_test.go | 42 +++++++++++++++++++++ 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/runtime/interpreter/interpreter.go b/runtime/interpreter/interpreter.go index 4ee5689bd0..3e3f76dbf9 100644 --- a/runtime/interpreter/interpreter.go +++ b/runtime/interpreter/interpreter.go @@ -991,23 +991,11 @@ func (interpreter *Interpreter) declareAttachmentValue( func (declarationInterpreter *Interpreter) evaluateDefaultDestroyEvent( containingResourceComposite *CompositeValue, eventDecl *ast.CompositeDeclaration, - invocation Invocation, - invocationActivation *VariableActivation, + declarationActivation *VariableActivation, ) (arguments []Value) { parameters := eventDecl.DeclarationMembers().Initializers()[0].FunctionDeclaration.ParameterList.Parameters - declarationInterpreter.activations.PushNewWithParent(invocationActivation) - declarationInterpreter.SharedState.callStack.Push(invocation) - - // interpreter.activations.PushNewWithParent(inter.activations.CurrentOrNew()) - // interpreter.SharedState.callStack.Push(invocation) - defer func() { - // Only unwind the call stack if there was no error - if r := recover(); r != nil { - panic(r) - } - declarationInterpreter.SharedState.callStack.Pop() - }() + declarationInterpreter.activations.Push(declarationActivation) defer declarationInterpreter.activations.Pop() var self MemberAccessibleValue = containingResourceComposite @@ -1158,6 +1146,8 @@ func (declarationInterpreter *Interpreter) declareNonEnumCompositeValue( initializerType := compositeType.InitializerFunctionType() + declarationActivation := declarationInterpreter.activations.CurrentOrNew() + var initializerFunction FunctionValue if declaration.Kind() == common.CompositeKindEvent { initializerFunction = NewHostFunctionValue( @@ -1180,8 +1170,9 @@ func (declarationInterpreter *Interpreter) declareNonEnumCompositeValue( invocation.Arguments = declarationInterpreter.evaluateDefaultDestroyEvent( containerComposite, compositeDecl, - invocation, - invocationInterpreter.activations.CurrentOrNew(), + // to properly lexically scope the evaluation of default arguments, we capture the + // activations existing at the time when the event was defined and use them here + declarationActivation, ) } diff --git a/runtime/tests/interpreter/resources_test.go b/runtime/tests/interpreter/resources_test.go index 4c8f749a2a..bdd37b0d76 100644 --- a/runtime/tests/interpreter/resources_test.go +++ b/runtime/tests/interpreter/resources_test.go @@ -2459,3 +2459,45 @@ func TestInterpretResourceInterfaceDefaultDestroyEventNoCompositeEvent(t *testin require.Equal(t, events[0].QualifiedIdentifier, "I.ResourceDestroyed") require.Equal(t, events[0].GetField(inter, interpreter.EmptyLocationRange, "id"), interpreter.NewIntValueFromInt64(nil, 1)) } + +func TestInterpretDefaultDestroyEventArgumentScoping(t *testing.T) { + + t.Parallel() + + var events []*interpreter.CompositeValue + + inter, err := parseCheckAndInterpretWithOptions(t, ` + let x = 1 + + resource R { + event ResourceDestroyed(x: Int = x) + } + + fun test() { + let x = 2 + let r <- create R() + // should emit R.ResourceDestroyed(x: 1), not R.ResourceDestroyed(x: 2) + destroy r + } + `, ParseCheckAndInterpretOptions{ + Config: &interpreter.Config{ + OnEventEmitted: func(inter *interpreter.Interpreter, locationRange interpreter.LocationRange, event *interpreter.CompositeValue, eventType *sema.CompositeType) error { + events = append(events, event) + return nil + }, + }, + HandleCheckerError: func(err error) { + errs := checker.RequireCheckerErrors(t, err, 1) + assert.IsType(t, &sema.DefaultDestroyInvalidArgumentError{}, errs[0]) + // ... + }, + }) + + require.NoError(t, err) + _, err = inter.Invoke("test") + require.NoError(t, err) + + require.Len(t, events, 1) + require.Equal(t, events[0].QualifiedIdentifier, "R.ResourceDestroyed") + require.Equal(t, events[0].GetField(inter, interpreter.EmptyLocationRange, "x"), interpreter.NewIntValueFromInt64(nil, 1)) +} From 920ea9428fa9c3e55305e5faf7ff1f48220033b6 Mon Sep 17 00:00:00 2001 From: Daniel Sainati Date: Thu, 2 Nov 2023 11:37:34 -0400 Subject: [PATCH 15/16] use new activation for default event param evaluation --- runtime/interpreter/interpreter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/interpreter/interpreter.go b/runtime/interpreter/interpreter.go index 3e3f76dbf9..4813f54ee1 100644 --- a/runtime/interpreter/interpreter.go +++ b/runtime/interpreter/interpreter.go @@ -995,7 +995,7 @@ func (declarationInterpreter *Interpreter) evaluateDefaultDestroyEvent( ) (arguments []Value) { parameters := eventDecl.DeclarationMembers().Initializers()[0].FunctionDeclaration.ParameterList.Parameters - declarationInterpreter.activations.Push(declarationActivation) + declarationInterpreter.activations.PushNewWithParent(declarationActivation) defer declarationInterpreter.activations.Pop() var self MemberAccessibleValue = containingResourceComposite From 89fbe55d33ab2d6dead499d76c4a622113da6f81 Mon Sep 17 00:00:00 2001 From: Daniel Sainati Date: Tue, 7 Nov 2023 13:58:01 -0500 Subject: [PATCH 16/16] style --- runtime/interpreter/interpreter.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/runtime/interpreter/interpreter.go b/runtime/interpreter/interpreter.go index 4813f54ee1..2b2c471bac 100644 --- a/runtime/interpreter/interpreter.go +++ b/runtime/interpreter/interpreter.go @@ -988,11 +988,13 @@ func (interpreter *Interpreter) declareAttachmentValue( // and c) functions cannot currently be explicitly invoked if they have default arguments // // if we plan to generalize this further, we will need to relax those assumptions -func (declarationInterpreter *Interpreter) evaluateDefaultDestroyEvent( +func (interpreter *Interpreter) evaluateDefaultDestroyEvent( containingResourceComposite *CompositeValue, eventDecl *ast.CompositeDeclaration, declarationActivation *VariableActivation, ) (arguments []Value) { + + declarationInterpreter := interpreter parameters := eventDecl.DeclarationMembers().Initializers()[0].FunctionDeclaration.ParameterList.Parameters declarationInterpreter.activations.PushNewWithParent(declarationActivation)