diff --git a/bbq/compiler/compiler_determinism_test.go b/bbq/compiler/compiler_determinism_test.go new file mode 100644 index 000000000..7daf90073 --- /dev/null +++ b/bbq/compiler/compiler_determinism_test.go @@ -0,0 +1,90 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Flow Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package compiler_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/onflow/cadence/bbq/compiler" + "github.com/onflow/cadence/interpreter" + . "github.com/onflow/cadence/test_utils/sema_utils" +) + +func compileQualifiedFunctionNames(t *testing.T, code string) []string { + checker, err := ParseAndCheck(t, code) + require.NoError(t, err) + + comp := compiler.NewInstructionCompiler( + interpreter.ProgramFromChecker(checker), + checker.Location, + ) + program := comp.Compile() + + names := make([]string, 0, len(program.Functions)) + for _, f := range program.Functions { + names = append(names, f.QualifiedName) + } + return names +} + +// TestCompilationDeterminismInheritedDefaultFunctions ensures that compiling the +// same program repeatedly always produces the functions (and therefore the +// global indices) in the same order. +// +// Inherited default-function delegators used to be generated by iterating over a +// Go map (`Members.FunctionsByIdentifier()`), whose iteration order is +// randomized. That made the order of the generated functions - and hence the +// compiled program - non-deterministic, which is unacceptable when the compiled +// output must be reproducible. The desugar phase now iterates the ordered +// `Members.Functions()` slice instead. +func TestCompilationDeterminismInheritedDefaultFunctions(t *testing.T) { + t.Parallel() + + code := ` + struct interface I { + fun a(): Int { return 1 } + fun b(): Int { return 2 } + fun c(): Int { return 3 } + fun d(): Int { return 4 } + fun e(): Int { return 5 } + fun f(): Int { return 6 } + fun g(): Int { return 7 } + fun h(): Int { return 8 } + } + + struct interface J { + fun p(): Int { return 10 } + fun q(): Int { return 20 } + fun r(): Int { return 30 } + } + + struct S: I, J {} + ` + + first := compileQualifiedFunctionNames(t, code) + + // Many repetitions, since map-iteration order is randomized per range and a + // single mismatch is enough to prove non-determinism. + for i := range 200 { + next := compileQualifiedFunctionNames(t, code) + require.Equal(t, first, next, "compilation %d produced a different function order", i) + } +} diff --git a/bbq/compiler/desugar.go b/bbq/compiler/desugar.go index 916344843..73757d5a1 100644 --- a/bbq/compiler/desugar.go +++ b/bbq/compiler/desugar.go @@ -1250,13 +1250,21 @@ func (d *Desugar) inheritedDefaultFunctions( } interfaceDecl := elaboration.InterfaceTypeDeclaration(interfaceType) - functions := interfaceDecl.Members.FunctionsByIdentifier() - for funcName, inheritedFunc := range functions { // nolint:maprange + // Iterate the functions in their declaration order (a stable slice), + // rather than over the identifier-keyed map, to keep the order of the + // generated delegator functions deterministic. A non-deterministic order + // would produce non-deterministic global indices, and therefore + // non-reproducible compiled programs. + functions := interfaceDecl.Members.Functions() + + for _, inheritedFunc := range functions { if !inheritedFunc.FunctionBlock.HasStatements() { continue } + funcName := inheritedFunc.Identifier.Identifier + // Pick the 'closest' default function. // This is the same way how it is implemented in the interpreter. _, ok := inheritedDefaultFunctions[funcName]