diff --git a/sema/check_import_declaration.go b/sema/check_import_declaration.go index 2372c4fbd..5754101e4 100644 --- a/sema/check_import_declaration.go +++ b/sema/check_import_declaration.go @@ -464,6 +464,7 @@ func (checker *Checker) importElements( isConstant: true, argumentLabels: element.ArgumentLabels, allowOuterScopeShadowing: false, + docString: element.DocString, }) checker.report(err) }) diff --git a/sema/import.go b/sema/import.go index dfbee83c6..d99a01160 100644 --- a/sema/import.go +++ b/sema/import.go @@ -37,6 +37,7 @@ type ImportElement struct { ArgumentLabels []string DeclarationKind common.DeclarationKind Access Access + DocString string } // ElaborationImport @@ -55,6 +56,7 @@ func variablesToImportElements(f func(func(name string, variable *Variable))) *S Access: variable.Access, Type: variable.Type, ArgumentLabels: variable.ArgumentLabels, + DocString: variable.DocString, }) }) diff --git a/stdlib/comparison.go b/stdlib/comparison.go new file mode 100644 index 000000000..95534b2a7 --- /dev/null +++ b/stdlib/comparison.go @@ -0,0 +1,396 @@ +/* + * 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 stdlib + +import ( + "fmt" + + "github.com/onflow/cadence/ast" + "github.com/onflow/cadence/common" + "github.com/onflow/cadence/errors" + "github.com/onflow/cadence/interpreter" + "github.com/onflow/cadence/sema" +) + +const ComparisonContractLocation = common.IdentifierLocation("Comparison") + +var ComparisonContractSemaImport = sema.VirtualImport{ + ValueElements: func() *sema.StringImportElementOrderedMap { + elements := &sema.StringImportElementOrderedMap{} + elements.Set( + minFunctionName, + sema.ImportElement{ + Type: minFunctionType, + DeclarationKind: common.DeclarationKindFunction, + Access: sema.PrimitiveAccess(ast.AccessAll), + DocString: minFunctionDocString, + }, + ) + elements.Set( + maxFunctionName, + sema.ImportElement{ + Type: maxFunctionType, + DeclarationKind: common.DeclarationKindFunction, + Access: sema.PrimitiveAccess(ast.AccessAll), + DocString: maxFunctionDocString, + }, + ) + elements.Set( + clampFunctionName, + sema.ImportElement{ + Type: clampFunctionType, + DeclarationKind: common.DeclarationKindFunction, + Access: sema.PrimitiveAccess(ast.AccessAll), + DocString: clampFunctionDocString, + }, + ) + return elements + }(), +} + +var ComparisonContractInterpreterImport = interpreter.VirtualImport{ + Globals: []interpreter.VirtualImportGlobal{ + { + Name: minFunctionName, + Value: interpreter.NewStaticHostFunctionValueFromNativeFunction( + nil, + minFunctionType, + NativeMinFunction, + ), + }, + { + Name: maxFunctionName, + Value: interpreter.NewStaticHostFunctionValueFromNativeFunction( + nil, + maxFunctionType, + NativeMaxFunction, + ), + }, + { + Name: clampFunctionName, + Value: interpreter.NewStaticHostFunctionValueFromNativeFunction( + nil, + clampFunctionType, + NativeClampFunction, + ), + }, + }, +} + +// MinFunction + +const minFunctionName = "min" + +const minFunctionDocString = ` +Returns the minimum of the two given values. +The arguments must be of the same comparable type. +` + +var minFunctionType = func() *sema.FunctionType { + typeParameter := &sema.TypeParameter{ + Name: "T", + // No TypeBound - we check comparability in TypeArgumentsCheck + } + + typeAnnotation := sema.NewTypeAnnotation( + &sema.GenericType{ + TypeParameter: typeParameter, + }, + ) + + return &sema.FunctionType{ + Purity: sema.FunctionPurityView, + TypeParameters: []*sema.TypeParameter{ + typeParameter, + }, + Parameters: []sema.Parameter{ + { + Label: sema.ArgumentLabelNotRequired, + Identifier: "a", + TypeAnnotation: typeAnnotation, + }, + { + Label: sema.ArgumentLabelNotRequired, + Identifier: "b", + TypeAnnotation: typeAnnotation, + }, + }, + ReturnTypeAnnotation: typeAnnotation, + TypeArgumentsCheck: func( + memoryGauge common.MemoryGauge, + typeArguments *sema.TypeParameterTypeOrderedMap, + _ []*ast.TypeAnnotation, + invocationRange ast.HasPosition, + report func(err error), + ) { + typeArg, ok := typeArguments.Get(typeParameter) + if !ok || typeArg == nil { + // Invalid, already reported by checker + return + } + + if !typeArg.IsComparable() { + report(&sema.InvalidTypeArgumentError{ + TypeArgumentName: typeParameter.Name, + Range: ast.NewRangeFromPositioned(memoryGauge, invocationRange), + Details: fmt.Sprintf( + "Type argument for `%s` must be a comparable type, got `%s`", + minFunctionName, + typeArg, + ), + }) + } + }, + } +}() + +var NativeMinFunction = interpreter.NativeFunction( + func( + context interpreter.NativeFunctionContext, + _ interpreter.TypeArgumentsIterator, + _ interpreter.ArgumentTypesIterator, + _ interpreter.Value, + args []interpreter.Value, + ) interpreter.Value { + a := args[0] + b := args[1] + + comparableA, ok := a.(interpreter.ComparableValue) + if !ok { + panic(errors.NewUnreachableError()) + } + + comparableB, ok := b.(interpreter.ComparableValue) + if !ok { + panic(errors.NewUnreachableError()) + } + + if comparableA.Less(context, comparableB) { + return a + } + return b + }, +) + +// MaxFunction + +const maxFunctionName = "max" + +const maxFunctionDocString = ` +Returns the maximum of the two given values. +The arguments must be of the same comparable type. +` + +var maxFunctionType = func() *sema.FunctionType { + typeParameter := &sema.TypeParameter{ + Name: "T", + // No TypeBound - we check comparability in TypeArgumentsCheck + } + + typeAnnotation := sema.NewTypeAnnotation( + &sema.GenericType{ + TypeParameter: typeParameter, + }, + ) + + return &sema.FunctionType{ + Purity: sema.FunctionPurityView, + TypeParameters: []*sema.TypeParameter{ + typeParameter, + }, + Parameters: []sema.Parameter{ + { + Label: sema.ArgumentLabelNotRequired, + Identifier: "a", + TypeAnnotation: typeAnnotation, + }, + { + Label: sema.ArgumentLabelNotRequired, + Identifier: "b", + TypeAnnotation: typeAnnotation, + }, + }, + ReturnTypeAnnotation: typeAnnotation, + TypeArgumentsCheck: func( + memoryGauge common.MemoryGauge, + typeArguments *sema.TypeParameterTypeOrderedMap, + _ []*ast.TypeAnnotation, + invocationRange ast.HasPosition, + report func(err error), + ) { + typeArg, ok := typeArguments.Get(typeParameter) + if !ok || typeArg == nil { + // Invalid, already reported by checker + return + } + + if !typeArg.IsComparable() { + report(&sema.InvalidTypeArgumentError{ + TypeArgumentName: typeParameter.Name, + Range: ast.NewRangeFromPositioned(memoryGauge, invocationRange), + Details: fmt.Sprintf( + "Type argument for `%s` must be a comparable type, got `%s`", + maxFunctionName, + typeArg, + ), + }) + } + }, + } +}() + +var NativeMaxFunction = interpreter.NativeFunction( + func( + context interpreter.NativeFunctionContext, + _ interpreter.TypeArgumentsIterator, + _ interpreter.ArgumentTypesIterator, + _ interpreter.Value, + args []interpreter.Value, + ) interpreter.Value { + a := args[0] + b := args[1] + + comparableA, ok := a.(interpreter.ComparableValue) + if !ok { + panic(errors.NewUnreachableError()) + } + + comparableB, ok := b.(interpreter.ComparableValue) + if !ok { + panic(errors.NewUnreachableError()) + } + + if comparableA.Greater(context, comparableB) { + return a + } + return b + }, +) + +// ClampFunction + +const clampFunctionName = "clamp" + +const clampFunctionDocString = ` +Returns the value clamped to the inclusive range [min, max]. +If the value is less than min, min is returned. +If the value is greater than max, max is returned. +Otherwise, the value itself is returned. +The arguments must be of the same comparable type. +` + +var clampFunctionType = func() *sema.FunctionType { + typeParameter := &sema.TypeParameter{ + Name: "T", + // No TypeBound - we check comparability in TypeArgumentsCheck + } + + typeAnnotation := sema.NewTypeAnnotation( + &sema.GenericType{ + TypeParameter: typeParameter, + }, + ) + + return &sema.FunctionType{ + Purity: sema.FunctionPurityView, + TypeParameters: []*sema.TypeParameter{ + typeParameter, + }, + Parameters: []sema.Parameter{ + { + Label: sema.ArgumentLabelNotRequired, + Identifier: "value", + TypeAnnotation: typeAnnotation, + }, + { + Label: "min", + Identifier: "min", + TypeAnnotation: typeAnnotation, + }, + { + Label: "max", + Identifier: "max", + TypeAnnotation: typeAnnotation, + }, + }, + ReturnTypeAnnotation: typeAnnotation, + TypeArgumentsCheck: func( + memoryGauge common.MemoryGauge, + typeArguments *sema.TypeParameterTypeOrderedMap, + _ []*ast.TypeAnnotation, + invocationRange ast.HasPosition, + report func(err error), + ) { + typeArg, ok := typeArguments.Get(typeParameter) + if !ok || typeArg == nil { + // Invalid, already reported by checker + return + } + + if !typeArg.IsComparable() { + report(&sema.InvalidTypeArgumentError{ + TypeArgumentName: typeParameter.Name, + Range: ast.NewRangeFromPositioned(memoryGauge, invocationRange), + Details: fmt.Sprintf( + "Type argument for `%s` must be a comparable type, got `%s`", + clampFunctionName, + typeArg, + ), + }) + } + }, + } +}() + +var NativeClampFunction = interpreter.NativeFunction( + func( + context interpreter.NativeFunctionContext, + _ interpreter.TypeArgumentsIterator, + _ interpreter.ArgumentTypesIterator, + _ interpreter.Value, + args []interpreter.Value, + ) interpreter.Value { + value := args[0] + min := args[1] + max := args[2] + + comparableValue, ok := value.(interpreter.ComparableValue) + if !ok { + panic(errors.NewUnreachableError()) + } + + comparableMin, ok := min.(interpreter.ComparableValue) + if !ok { + panic(errors.NewUnreachableError()) + } + + comparableMax, ok := max.(interpreter.ComparableValue) + if !ok { + panic(errors.NewUnreachableError()) + } + + if comparableValue.Less(context, comparableMin) { + return min + } + if comparableValue.Greater(context, comparableMax) { + return max + } + return value + }, +) diff --git a/stdlib/comparison_test.go b/stdlib/comparison_test.go new file mode 100644 index 000000000..46d2e84ea --- /dev/null +++ b/stdlib/comparison_test.go @@ -0,0 +1,558 @@ +/* + * 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 stdlib + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/onflow/cadence/ast" + "github.com/onflow/cadence/common" + "github.com/onflow/cadence/interpreter" + "github.com/onflow/cadence/parser" + "github.com/onflow/cadence/sema" + . "github.com/onflow/cadence/test_utils/common_utils" + . "github.com/onflow/cadence/test_utils/interpreter_utils" + . "github.com/onflow/cadence/test_utils/sema_utils" +) + +func TestMinFunction(t *testing.T) { + t.Parallel() + + parseAndCheck := func(t *testing.T, code string) (*sema.Checker, error) { + return ParseAndCheckWithOptions(t, + code, + ParseAndCheckOptions{ + CheckerConfig: &sema.Config{ + ImportHandler: func( + _ *sema.Checker, + importedLocation common.Location, + _ ast.Range, + ) (sema.Import, error) { + if importedLocation == ComparisonContractLocation { + return ComparisonContractSemaImport, nil + } + return nil, fmt.Errorf("unexpected import: %s", importedLocation) + }, + }, + }, + ) + } + + t.Run("Int", func(t *testing.T) { + t.Parallel() + + _, err := parseAndCheck(t, ` + import Comparison + + let result: Int = min(5, 10) + `) + + require.NoError(t, err) + }) + + t.Run("Int8", func(t *testing.T) { + t.Parallel() + + _, err := parseAndCheck(t, ` + import Comparison + + let result: Int8 = min(5, 10) + `) + + require.NoError(t, err) + }) + + t.Run("UFix64", func(t *testing.T) { + t.Parallel() + + _, err := parseAndCheck(t, ` + import Comparison + + let result: UFix64 = min(5.5, 10.5) + `) + + require.NoError(t, err) + }) + + t.Run("String", func(t *testing.T) { + t.Parallel() + + _, err := parseAndCheck(t, ` + import Comparison + + let result: String = min("a", "b") + `) + + require.NoError(t, err) + }) + + t.Run("non-comparable type", func(t *testing.T) { + t.Parallel() + + _, err := parseAndCheck(t, ` + import Comparison + + fun foo(): Void {} + fun bar(): Void {} + let result = min(foo, bar) + `) + + errs := RequireCheckerErrors(t, err, 1) + require.IsType(t, &sema.InvalidTypeArgumentError{}, errs[0]) + }) + + t.Run("mismatched types", func(t *testing.T) { + t.Parallel() + + _, err := parseAndCheck(t, ` + import Comparison + + let result = min(5, 10.5) + `) + + errs := RequireCheckerErrors(t, err, 1) + require.IsType(t, &sema.TypeMismatchError{}, errs[0]) + }) +} + +func TestMaxFunction(t *testing.T) { + t.Parallel() + + parseAndCheck := func(t *testing.T, code string) (*sema.Checker, error) { + return ParseAndCheckWithOptions(t, + code, + ParseAndCheckOptions{ + CheckerConfig: &sema.Config{ + ImportHandler: func( + _ *sema.Checker, + importedLocation common.Location, + _ ast.Range, + ) (sema.Import, error) { + if importedLocation == ComparisonContractLocation { + return ComparisonContractSemaImport, nil + } + return nil, fmt.Errorf("unexpected import: %s", importedLocation) + }, + }, + }, + ) + } + + t.Run("Int", func(t *testing.T) { + t.Parallel() + + _, err := parseAndCheck(t, ` + import Comparison + + let result: Int = max(5, 10) + `) + + require.NoError(t, err) + }) + + t.Run("Int16", func(t *testing.T) { + t.Parallel() + + _, err := parseAndCheck(t, ` + import Comparison + + let result: Int16 = max(5, 10) + `) + + require.NoError(t, err) + }) + + t.Run("Fix64", func(t *testing.T) { + t.Parallel() + + _, err := parseAndCheck(t, ` + import Comparison + + let result: Fix64 = max(5.5, 10.5) + `) + + require.NoError(t, err) + }) + + t.Run("String", func(t *testing.T) { + t.Parallel() + + _, err := parseAndCheck(t, ` + import Comparison + + let result: String = max("a", "b") + `) + + require.NoError(t, err) + }) + + t.Run("non-comparable type", func(t *testing.T) { + t.Parallel() + + _, err := parseAndCheck(t, ` + import Comparison + + let result = max<{String: Int}>({}, {}) + `) + + errs := RequireCheckerErrors(t, err, 1) + require.IsType(t, &sema.InvalidTypeArgumentError{}, errs[0]) + }) + + t.Run("mismatched types", func(t *testing.T) { + t.Parallel() + + _, err := parseAndCheck(t, ` + import Comparison + + let result = max(5.5, 10) + `) + + errs := RequireCheckerErrors(t, err, 1) + require.IsType(t, &sema.TypeMismatchError{}, errs[0]) + }) +} + +// TODO: test with compiler/VM +func newInterpreterWithComparison(t *testing.T, code string) *interpreter.Interpreter { + program, err := parser.ParseProgram( + nil, + []byte(code), + parser.Config{}, + ) + require.NoError(t, err) + + checker, err := sema.NewChecker( + program, + TestLocation, + nil, + &sema.Config{ + ImportHandler: func( + _ *sema.Checker, + importedLocation common.Location, + _ ast.Range, + ) (sema.Import, error) { + if importedLocation == ComparisonContractLocation { + return ComparisonContractSemaImport, nil + } + return nil, fmt.Errorf("unexpected import: %s", importedLocation) + }, + AccessCheckMode: sema.AccessCheckModeStrict, + }, + ) + require.NoError(t, err) + + err = checker.Check() + require.NoError(t, err) + + storage := NewUnmeteredInMemoryStorage() + + inter, err := interpreter.NewInterpreter( + interpreter.ProgramFromChecker(checker), + checker.Location, + &interpreter.Config{ + Storage: storage, + ImportLocationHandler: func(inter *interpreter.Interpreter, location common.Location) interpreter.Import { + if location == ComparisonContractLocation { + return ComparisonContractInterpreterImport + } + return nil + }, + }, + ) + require.NoError(t, err) + + err = inter.Interpret() + require.NoError(t, err) + + return inter +} + +func TestMinFunctionRuntime(t *testing.T) { + t.Parallel() + + t.Run("Int", func(t *testing.T) { + t.Parallel() + + inter := newInterpreterWithComparison(t, ` + import Comparison + + access(all) let result = min(5, 10) + `) + + result := inter.Globals.Get("result").GetValue(inter) + require.Equal(t, interpreter.NewUnmeteredIntValueFromInt64(5), result) + }) + + t.Run("Int, reversed", func(t *testing.T) { + t.Parallel() + + inter := newInterpreterWithComparison(t, ` + import Comparison + + access(all) let result = min(10, 5) + `) + + result := inter.Globals.Get("result").GetValue(inter) + require.Equal(t, interpreter.NewUnmeteredIntValueFromInt64(5), result) + }) + + t.Run("UFix64, explicit type argument", func(t *testing.T) { + t.Parallel() + + inter := newInterpreterWithComparison(t, ` + import Comparison + + access(all) let result = min(5.5, 10.5) + `) + + result := inter.Globals.Get("result").GetValue(inter) + expected := interpreter.NewUnmeteredUFix64Value(550_000_000) + assert.Equal(t, expected, result) + }) +} + +func TestMaxFunctionRuntime(t *testing.T) { + t.Parallel() + + t.Run("Int", func(t *testing.T) { + t.Parallel() + + inter := newInterpreterWithComparison(t, ` + import Comparison + + access(all) let result = max(5, 10) + `) + + result := inter.Globals.Get("result").GetValue(inter) + require.Equal(t, interpreter.NewUnmeteredIntValueFromInt64(10), result) + }) + + t.Run("Int, reversed", func(t *testing.T) { + t.Parallel() + + inter := newInterpreterWithComparison(t, ` + import Comparison + + access(all) let result = max(10, 5) + `) + + result := inter.Globals.Get("result").GetValue(inter) + require.Equal(t, interpreter.NewUnmeteredIntValueFromInt64(10), result) + }) + + t.Run("UFix64, explicit type argument", func(t *testing.T) { + t.Parallel() + + inter := newInterpreterWithComparison(t, ` + import Comparison + + access(all) let result = max(5.5, 10.5) + `) + + result := inter.Globals.Get("result").GetValue(inter) + expected := interpreter.NewUnmeteredUFix64Value(1_050_000_000) + assert.Equal(t, expected, result) + }) +} + +func TestClampFunction(t *testing.T) { + t.Parallel() + + parseAndCheck := func(t *testing.T, code string) (*sema.Checker, error) { + return ParseAndCheckWithOptions(t, + code, + ParseAndCheckOptions{ + CheckerConfig: &sema.Config{ + ImportHandler: func( + _ *sema.Checker, + importedLocation common.Location, + _ ast.Range, + ) (sema.Import, error) { + if importedLocation == ComparisonContractLocation { + return ComparisonContractSemaImport, nil + } + return nil, fmt.Errorf("unexpected import: %s", importedLocation) + }, + }, + }, + ) + } + + t.Run("Int", func(t *testing.T) { + t.Parallel() + + _, err := parseAndCheck(t, ` + import Comparison + + let result: Int = clamp(7, min: 1, max: 10) + `) + + require.NoError(t, err) + }) + + t.Run("Int8", func(t *testing.T) { + t.Parallel() + + _, err := parseAndCheck(t, ` + import Comparison + + let result: Int8 = clamp(7, min: 1, max: 10) + `) + + require.NoError(t, err) + }) + + t.Run("UFix64", func(t *testing.T) { + t.Parallel() + + _, err := parseAndCheck(t, ` + import Comparison + + let result: UFix64 = clamp(7.5, min: 1.0, max: 10.0) + `) + + require.NoError(t, err) + }) + + t.Run("String", func(t *testing.T) { + t.Parallel() + + _, err := parseAndCheck(t, ` + import Comparison + + let result: String = clamp("d", min: "a", max: "f") + `) + + require.NoError(t, err) + }) + + t.Run("non-comparable type", func(t *testing.T) { + t.Parallel() + + _, err := parseAndCheck(t, ` + import Comparison + + let result = clamp<{String: Int}>({}, min: {}, max: {}) + `) + + errs := RequireCheckerErrors(t, err, 1) + require.IsType(t, &sema.InvalidTypeArgumentError{}, errs[0]) + }) + + t.Run("mismatched types", func(t *testing.T) { + t.Parallel() + + _, err := parseAndCheck(t, ` + import Comparison + + let result = clamp(5, min: 1, max: 10.0) + `) + + errs := RequireCheckerErrors(t, err, 1) + require.IsType(t, &sema.TypeMismatchError{}, errs[0]) + }) +} + +func TestClampFunctionRuntime(t *testing.T) { + t.Parallel() + + t.Run("Int, within range", func(t *testing.T) { + t.Parallel() + + inter := newInterpreterWithComparison(t, ` + import Comparison + + access(all) let result = clamp(7, min: 1, max: 10) + `) + + result := inter.Globals.Get("result").GetValue(inter) + require.Equal(t, interpreter.NewUnmeteredIntValueFromInt64(7), result) + }) + + t.Run("Int, below min", func(t *testing.T) { + t.Parallel() + + inter := newInterpreterWithComparison(t, ` + import Comparison + + access(all) let result = clamp(0, min: 1, max: 10) + `) + + result := inter.Globals.Get("result").GetValue(inter) + require.Equal(t, interpreter.NewUnmeteredIntValueFromInt64(1), result) + }) + + t.Run("Int, above max", func(t *testing.T) { + t.Parallel() + + inter := newInterpreterWithComparison(t, ` + import Comparison + + access(all) let result = clamp(20, min: 1, max: 10) + `) + + result := inter.Globals.Get("result").GetValue(inter) + require.Equal(t, interpreter.NewUnmeteredIntValueFromInt64(10), result) + }) + + t.Run("Int, equal to min", func(t *testing.T) { + t.Parallel() + + inter := newInterpreterWithComparison(t, ` + import Comparison + + access(all) let result = clamp(1, min: 1, max: 10) + `) + + result := inter.Globals.Get("result").GetValue(inter) + require.Equal(t, interpreter.NewUnmeteredIntValueFromInt64(1), result) + }) + + t.Run("Int, equal to max", func(t *testing.T) { + t.Parallel() + + inter := newInterpreterWithComparison(t, ` + import Comparison + + access(all) let result = clamp(10, min: 1, max: 10) + `) + + result := inter.Globals.Get("result").GetValue(inter) + require.Equal(t, interpreter.NewUnmeteredIntValueFromInt64(10), result) + }) + + t.Run("UFix64, explicit type argument", func(t *testing.T) { + t.Parallel() + + inter := newInterpreterWithComparison(t, ` + import Comparison + + access(all) let result = clamp(7.5, min: 1.0, max: 10.0) + `) + + result := inter.Globals.Get("result").GetValue(inter) + expected := interpreter.NewUnmeteredUFix64Value(750_000_000) + assert.Equal(t, expected, result) + }) +} diff --git a/tools/analysis/programs.go b/tools/analysis/programs.go index c23643ba5..186477c6f 100644 --- a/tools/analysis/programs.go +++ b/tools/analysis/programs.go @@ -159,6 +159,8 @@ func (programs *Programs) check( var loadError error switch importedLocation { + case stdlib.ComparisonContractLocation: + return stdlib.ComparisonContractSemaImport, nil case stdlib.CryptoContractLocation: // If the elaboration for the crypto contract is available, take it. elaboration = programs.CryptoContractElaboration