diff --git a/runtime/interpreter/interpreter.go b/runtime/interpreter/interpreter.go index 0362108d69..584337f860 100644 --- a/runtime/interpreter/interpreter.go +++ b/runtime/interpreter/interpreter.go @@ -1267,16 +1267,7 @@ func (declarationInterpreter *Interpreter) declareNonEnumCompositeValue( functions.Set(resourceDefaultDestroyEventName(compositeType), destroyEventConstructor) } - wrapFunctions := func(ty *sema.InterfaceType, code WrapperCode) { - - // Wrap initializer - - initializerFunctionWrapper := - code.InitializerFunctionWrapper - - if initializerFunctionWrapper != nil { - initializerFunction = initializerFunctionWrapper(initializerFunction) - } + applyDefaultFunctions := func(ty *sema.InterfaceType, code WrapperCode) { // Apply default functions, if conforming type does not provide the function @@ -1295,6 +1286,18 @@ func (declarationInterpreter *Interpreter) declareNonEnumCompositeValue( functions.Set(name, function) }) } + } + + wrapFunctions := func(ty *sema.InterfaceType, code WrapperCode) { + + // Wrap initializer + + initializerFunctionWrapper := + code.InitializerFunctionWrapper + + if initializerFunctionWrapper != nil { + initializerFunction = initializerFunctionWrapper(initializerFunction) + } // Wrap functions @@ -1303,7 +1306,11 @@ func (declarationInterpreter *Interpreter) declareNonEnumCompositeValue( // the order does not matter. for name, functionWrapper := range code.FunctionWrappers { //nolint:maprange - fn, _ := functions.Get(name) + fn, ok := functions.Get(name) + // If there is a wrapper, there MUST be a body. + if !ok { + panic(errors.NewUnreachableError()) + } functions.Set(name, functionWrapper(fn)) } @@ -1313,9 +1320,21 @@ func (declarationInterpreter *Interpreter) declareNonEnumCompositeValue( } conformances := compositeType.EffectiveInterfaceConformances() + interfaceCodes := declarationInterpreter.SharedState.typeCodes.InterfaceCodes + + // First apply the default functions, and then wrap with conditions. + // These needs to be done in separate phases. + // Otherwise, if the condition and the default implementation are coming from two different inherited interfaces, + // then the condition would wrap an empty implementation, because the default impl is not resolved by the time. + + for i := len(conformances) - 1; i >= 0; i-- { + conformance := conformances[i].InterfaceType + applyDefaultFunctions(conformance, interfaceCodes[conformance.ID()]) + } + for i := len(conformances) - 1; i >= 0; i-- { conformance := conformances[i].InterfaceType - wrapFunctions(conformance, declarationInterpreter.SharedState.typeCodes.InterfaceCodes[conformance.ID()]) + wrapFunctions(conformance, interfaceCodes[conformance.ID()]) } declarationInterpreter.SharedState.typeCodes.CompositeCodes[compositeType.ID()] = CompositeTypeCode{ @@ -2451,6 +2470,13 @@ func (interpreter *Interpreter) functionConditionsWrapper( } return func(inner FunctionValue) FunctionValue { + + // NOTE: The `inner` function cannot be nil. + // An executing function always have a body. + if inner == nil { + panic(errors.NewUnreachableError()) + } + // Condition wrapper is a static function. return NewStaticHostFunctionValue( interpreter, @@ -2476,72 +2502,66 @@ func (interpreter *Interpreter) functionConditionsWrapper( interpreter.declareVariable(sema.BaseIdentifier, invocation.Base) } - // NOTE: The `inner` function might be nil. - // This is the case if the conforming type did not declare a function. - - var body func() StatementResult - if inner != nil { - // NOTE: It is important to wrap the invocation in a function, - // so the inner function isn't invoked here - - body = func() StatementResult { - - // Pre- and post-condition wrappers "re-declare" the same - // parameters as are used in the actual body of the function, - // see the use of bindParameterArguments at the start of this function wrapper. - // - // When these parameters are given resource-kinded arguments, - // this can trick the resource analysis into believing that these - // resources exist in multiple variables at once - // (one for each condition wrapper + the function itself). - // - // This is not the case, however, as execution of the pre- and post-conditions - // occurs strictly before and after execution of the body respectively. - // - // To prevent the analysis from reporting a false positive here, - // when we enter the body of the wrapped function, - // we invalidate any resources that were assigned to parameters by the precondition block, - // and then restore them after execution of the wrapped function, - // for use by the post-condition block. - - type argumentVariable struct { - variable Variable - value ResourceKindedValue + // NOTE: It is important to wrap the invocation in a function, + // so the inner function isn't invoked here + + body := func() StatementResult { + + // Pre- and post-condition wrappers "re-declare" the same + // parameters as are used in the actual body of the function, + // see the use of bindParameterArguments at the start of this function wrapper. + // + // When these parameters are given resource-kinded arguments, + // this can trick the resource analysis into believing that these + // resources exist in multiple variables at once + // (one for each condition wrapper + the function itself). + // + // This is not the case, however, as execution of the pre- and post-conditions + // occurs strictly before and after execution of the body respectively. + // + // To prevent the analysis from reporting a false positive here, + // when we enter the body of the wrapped function, + // we invalidate any resources that were assigned to parameters by the precondition block, + // and then restore them after execution of the wrapped function, + // for use by the post-condition block. + + type argumentVariable struct { + variable Variable + value ResourceKindedValue + } + + var argumentVariables []argumentVariable + for _, argument := range invocation.Arguments { + resourceKindedValue := interpreter.resourceForValidation(argument) + if resourceKindedValue == nil { + continue } - var argumentVariables []argumentVariable - for _, argument := range invocation.Arguments { - resourceKindedValue := interpreter.resourceForValidation(argument) - if resourceKindedValue == nil { - continue - } - - argumentVariables = append( - argumentVariables, - argumentVariable{ - variable: interpreter.SharedState.resourceVariables[resourceKindedValue], - value: resourceKindedValue, - }, - ) + argumentVariables = append( + argumentVariables, + argumentVariable{ + variable: interpreter.SharedState.resourceVariables[resourceKindedValue], + value: resourceKindedValue, + }, + ) - interpreter.invalidateResource(resourceKindedValue) - } + interpreter.invalidateResource(resourceKindedValue) + } - // NOTE: It is important to actually return the value returned - // from the inner function, otherwise it is lost + // NOTE: It is important to actually return the value returned + // from the inner function, otherwise it is lost - returnValue := inner.invoke(invocation) + returnValue := inner.invoke(invocation) - // Restore the resources which were temporarily invalidated - // before execution of the inner function + // Restore the resources which were temporarily invalidated + // before execution of the inner function - for _, argumentVariable := range argumentVariables { - value := argumentVariable.value - interpreter.invalidateResource(value) - interpreter.SharedState.resourceVariables[value] = argumentVariable.variable - } - return ReturnResult{Value: returnValue} + for _, argumentVariable := range argumentVariables { + value := argumentVariable.value + interpreter.invalidateResource(value) + interpreter.SharedState.resourceVariables[value] = argumentVariable.variable } + return ReturnResult{Value: returnValue} } declarationLocationRange := LocationRange{ diff --git a/runtime/tests/interpreter/interface_test.go b/runtime/tests/interpreter/interface_test.go index d17c5d305d..22d3a3369b 100644 --- a/runtime/tests/interpreter/interface_test.go +++ b/runtime/tests/interpreter/interface_test.go @@ -871,6 +871,203 @@ func TestInterpretInterfaceFunctionConditionsInheritance(t *testing.T) { // A.Nested and B.Nested are two distinct separate functions assert.Equal(t, []string{"B"}, logs) }) + + t.Run("pre condition in parent, default impl in child", func(t *testing.T) { + + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + access(all) resource interface A { + access(all) fun get(): Int { + pre { + true + } + } + } + + access(all) resource interface B: A { + access(all) fun get(): Int { + return 4 + } + } + + access(all) resource R: B {} + + access(all) fun main(): Int { + let r <- create R() + let value = r.get() + destroy r + return value + } + `) + + value, err := inter.Invoke("main") + require.NoError(t, err) + + assert.Equal(t, + interpreter.NewUnmeteredIntValueFromInt64(4), + value, + ) + }) + + t.Run("post condition in parent, default impl in child", func(t *testing.T) { + + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + access(all) resource interface A { + access(all) fun get(): Int { + post { + true + } + } + } + + access(all) resource interface B: A { + access(all) fun get(): Int { + return 4 + } + } + + access(all) resource R: B {} + + access(all) fun main(): Int { + let r <- create R() + let value = r.get() + destroy r + return value + } + `) + + value, err := inter.Invoke("main") + require.NoError(t, err) + + assert.Equal(t, + interpreter.NewUnmeteredIntValueFromInt64(4), + value, + ) + }) + + t.Run("siblings with condition in first and default impl in second", func(t *testing.T) { + + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + access(all) struct interface A { + access(all) fun get(): Int { + post { true } + } + } + + access(all) struct interface B { + access(all) fun get(): Int { + return 4 + } + } + + struct interface C: A, B {} + + access(all) struct S: C {} + + access(all) fun main(): Int { + let s = S() + return s.get() + } + `) + + value, err := inter.Invoke("main") + require.NoError(t, err) + + assert.Equal(t, + interpreter.NewUnmeteredIntValueFromInt64(4), + value, + ) + }) + + t.Run("siblings with default impl in first and condition in second", func(t *testing.T) { + + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + access(all) struct interface A { + access(all) fun get(): Int { + return 4 + } + } + + access(all) struct interface B { + access(all) fun get(): Int { + post { true } + } + } + + struct interface C: A, B {} + + access(all) struct S: C {} + + access(all) fun main(): Int { + let s = S() + return s.get() + } + `) + + value, err := inter.Invoke("main") + require.NoError(t, err) + + assert.Equal(t, + interpreter.NewUnmeteredIntValueFromInt64(4), + value, + ) + }) + + t.Run("result variable in conditions", func(t *testing.T) { + + t.Parallel() + + inter, getLogs, err := parseCheckAndInterpretWithLogs(t, ` + access(all) resource interface I1 { + access(all) let s: String + + access(all) fun echo(_ s: String): String { + post { + result == self.s: "result must match stored input, got: ".concat(result) + } + } + } + + access(all) resource interface I2: I1 { + access(all) let s: String + + access(all) fun echo(_ s: String): String { + log(s) + return self.s + } + } + + access(all) resource R: I2 { + access(all) let s: String + + init() { + self.s = "hello" + } + } + + access(all) fun main() { + let r <- create R() + r.echo("hello") + destroy r + } + `) + require.NoError(t, err) + + _, err = inter.Invoke("main") + require.NoError(t, err) + + logs := getLogs() + require.Len(t, logs, 1) + assert.Equal(t, "\"hello\"", logs[0]) + }) + } func TestInterpretNestedInterfaceCast(t *testing.T) {