From 4bedb5af828a777cddf8fb31e4bdca385f503147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Thu, 5 Feb 2026 16:36:44 -0800 Subject: [PATCH 1/7] add min and max functions --- stdlib/builtin.go | 4 + stdlib/comparison.go | 250 ++++++++++++++++++++++++++++++++++ stdlib/comparison_test.go | 273 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 527 insertions(+) create mode 100644 stdlib/comparison.go create mode 100644 stdlib/comparison_test.go diff --git a/stdlib/builtin.go b/stdlib/builtin.go index 76d39f9ef5..62263f6388 100644 --- a/stdlib/builtin.go +++ b/stdlib/builtin.go @@ -58,6 +58,8 @@ func InterpreterDefaultStandardLibraryValues(handler StandardLibraryHandler) []S InterpreterPanicFunction, InterpreterSignatureAlgorithmConstructor, InterpreterInclusiveRangeConstructor, + InterpreterMinFunction, + InterpreterMaxFunction, NewInterpreterLogFunction(handler), NewInterpreterRevertibleRandomFunction(handler), NewInterpreterGetBlockFunction(handler), @@ -77,6 +79,8 @@ func VMDefaultStandardLibraryValues(handler StandardLibraryHandler) []StandardLi VMPanicFunction, VMSignatureAlgorithmConstructor, VMInclusiveRangeConstructor, + VMMinFunction, + VMMaxFunction, NewVMLogFunction(handler), NewVMRevertibleRandomFunction(handler), NewVMGetBlockFunction(handler), diff --git a/stdlib/comparison.go b/stdlib/comparison.go new file mode 100644 index 0000000000..6cd6e2c1aa --- /dev/null +++ b/stdlib/comparison.go @@ -0,0 +1,250 @@ +/* + * 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/interpreter" + "github.com/onflow/cadence/sema" +) + +// 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(fmt.Sprintf("min: first argument is not comparable: %T", a)) + } + + comparableB, ok := b.(interpreter.ComparableValue) + if !ok { + panic(fmt.Sprintf("min: second argument is not comparable: %T", b)) + } + + if bool(comparableA.Less(context, comparableB)) { + return a + } + return b + }, +) + +var InterpreterMinFunction = NewNativeStandardLibraryStaticFunction( + minFunctionName, + minFunctionType, + minFunctionDocString, + NativeMinFunction, + false, +) + +var VMMinFunction = NewNativeStandardLibraryStaticFunction( + minFunctionName, + minFunctionType, + minFunctionDocString, + NativeMinFunction, + true, +) + +// 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(fmt.Sprintf("max: first argument is not comparable: %T", a)) + } + + comparableB, ok := b.(interpreter.ComparableValue) + if !ok { + panic(fmt.Sprintf("max: second argument is not comparable: %T", b)) + } + + if bool(comparableA.Greater(context, comparableB)) { + return a + } + return b + }, +) + +var InterpreterMaxFunction = NewNativeStandardLibraryStaticFunction( + maxFunctionName, + maxFunctionType, + maxFunctionDocString, + NativeMaxFunction, + false, +) + +var VMMaxFunction = NewNativeStandardLibraryStaticFunction( + maxFunctionName, + maxFunctionType, + maxFunctionDocString, + NativeMaxFunction, + true, +) diff --git a/stdlib/comparison_test.go b/stdlib/comparison_test.go new file mode 100644 index 0000000000..f1e6631fb6 --- /dev/null +++ b/stdlib/comparison_test.go @@ -0,0 +1,273 @@ +/* + * 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 ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/onflow/cadence/common" + "github.com/onflow/cadence/interpreter" + "github.com/onflow/cadence/sema" + . "github.com/onflow/cadence/test_utils/sema_utils" +) + +func TestMinFunction(t *testing.T) { + t.Parallel() + + baseValueActivation := sema.NewVariableActivation(sema.BaseValueActivation) + baseValueActivation.DeclareValue(InterpreterMinFunction) + + parseAndCheck := func(t *testing.T, code string) (*sema.Checker, error) { + return ParseAndCheckWithOptions(t, + code, + ParseAndCheckOptions{ + CheckerConfig: &sema.Config{ + BaseValueActivationHandler: func(_ common.Location) *sema.VariableActivation { + return baseValueActivation + }, + }, + }, + ) + } + + t.Run("Int", func(t *testing.T) { + t.Parallel() + + _, err := parseAndCheck(t, ` + let result = min(5, 10) + `) + + require.NoError(t, err) + }) + + t.Run("Int8", func(t *testing.T) { + t.Parallel() + + _, err := parseAndCheck(t, ` + let result = min(5, 10) + `) + + require.NoError(t, err) + }) + + t.Run("UFix64", func(t *testing.T) { + t.Parallel() + + _, err := parseAndCheck(t, ` + let result = min(5.5, 10.5) + `) + + require.NoError(t, err) + }) + + t.Run("String", func(t *testing.T) { + t.Parallel() + + _, err := parseAndCheck(t, ` + let result = min("a", "b") + `) + + require.NoError(t, err) + }) + + t.Run("non-comparable type", func(t *testing.T) { + t.Parallel() + + _, err := parseAndCheck(t, ` + 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, ` + 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() + + baseValueActivation := sema.NewVariableActivation(sema.BaseValueActivation) + baseValueActivation.DeclareValue(InterpreterMaxFunction) + + parseAndCheck := func(t *testing.T, code string) (*sema.Checker, error) { + return ParseAndCheckWithOptions(t, + code, + ParseAndCheckOptions{ + CheckerConfig: &sema.Config{ + BaseValueActivationHandler: func(_ common.Location) *sema.VariableActivation { + return baseValueActivation + }, + }, + }, + ) + } + + t.Run("Int", func(t *testing.T) { + t.Parallel() + + _, err := parseAndCheck(t, ` + let result = max(5, 10) + `) + + require.NoError(t, err) + }) + + t.Run("Int16", func(t *testing.T) { + t.Parallel() + + _, err := parseAndCheck(t, ` + let result = max(5, 10) + `) + + require.NoError(t, err) + }) + + t.Run("Fix64", func(t *testing.T) { + t.Parallel() + + _, err := parseAndCheck(t, ` + let result = max(5.5, 10.5) + `) + + require.NoError(t, err) + }) + + t.Run("String", func(t *testing.T) { + t.Parallel() + + _, err := parseAndCheck(t, ` + let result = max("a", "b") + `) + + require.NoError(t, err) + }) + + t.Run("non-comparable type", func(t *testing.T) { + t.Parallel() + + _, err := parseAndCheck(t, ` + 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, ` + let result = max(5.5, 10) + `) + + errs := RequireCheckerErrors(t, err, 1) + require.IsType(t, &sema.TypeMismatchError{}, errs[0]) + }) +} + +func TestMinFunctionRuntime(t *testing.T) { + t.Parallel() + + t.Run("Int min", func(t *testing.T) { + t.Parallel() + + inter := newInterpreter(t, ` + access(all) let result = min(5, 10) + `, InterpreterMinFunction) + + result := inter.Globals.Get("result").GetValue(inter) + require.Equal(t, interpreter.NewUnmeteredIntValueFromInt64(5), result) + }) + + t.Run("Int max argument", func(t *testing.T) { + t.Parallel() + + inter := newInterpreter(t, ` + access(all) let result = min(10, 5) + `, InterpreterMinFunction) + + result := inter.Globals.Get("result").GetValue(inter) + require.Equal(t, interpreter.NewUnmeteredIntValueFromInt64(5), result) + }) + + t.Run("UFix64", func(t *testing.T) { + t.Parallel() + + inter := newInterpreter(t, ` + access(all) let result = min(5.5, 10.5) + `, InterpreterMinFunction) + + 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 max", func(t *testing.T) { + t.Parallel() + + inter := newInterpreter(t, ` + access(all) let result = max(5, 10) + `, InterpreterMaxFunction) + + result := inter.Globals.Get("result").GetValue(inter) + require.Equal(t, interpreter.NewUnmeteredIntValueFromInt64(10), result) + }) + + t.Run("Int min argument", func(t *testing.T) { + t.Parallel() + + inter := newInterpreter(t, ` + access(all) let result = max(10, 5) + `, InterpreterMaxFunction) + + result := inter.Globals.Get("result").GetValue(inter) + require.Equal(t, interpreter.NewUnmeteredIntValueFromInt64(10), result) + }) + + t.Run("UFix64", func(t *testing.T) { + t.Parallel() + + inter := newInterpreter(t, ` + access(all) let result = max(5.5, 10.5) + `, InterpreterMaxFunction) + + result := inter.Globals.Get("result").GetValue(inter) + expected := interpreter.NewUnmeteredUFix64Value(1_050_000_000) + assert.Equal(t, expected, result) + }) +} From 5e129344d4fd9fce064d3bee0f72b69c5361b7f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Mon, 9 Feb 2026 14:23:33 -0800 Subject: [PATCH 2/7] rename min/max to minOf/maxOf --- stdlib/builtin.go | 8 ++--- stdlib/comparison.go | 71 +++++++++++++++++++------------------- stdlib/comparison_test.go | 72 +++++++++++++++++++-------------------- 3 files changed, 76 insertions(+), 75 deletions(-) diff --git a/stdlib/builtin.go b/stdlib/builtin.go index 62263f6388..570a338187 100644 --- a/stdlib/builtin.go +++ b/stdlib/builtin.go @@ -58,8 +58,8 @@ func InterpreterDefaultStandardLibraryValues(handler StandardLibraryHandler) []S InterpreterPanicFunction, InterpreterSignatureAlgorithmConstructor, InterpreterInclusiveRangeConstructor, - InterpreterMinFunction, - InterpreterMaxFunction, + InterpreterMinOfFunction, + InterpreterMaxOfFunction, NewInterpreterLogFunction(handler), NewInterpreterRevertibleRandomFunction(handler), NewInterpreterGetBlockFunction(handler), @@ -79,8 +79,8 @@ func VMDefaultStandardLibraryValues(handler StandardLibraryHandler) []StandardLi VMPanicFunction, VMSignatureAlgorithmConstructor, VMInclusiveRangeConstructor, - VMMinFunction, - VMMaxFunction, + VMMinOfFunction, + VMMaxOfFunction, NewVMLogFunction(handler), NewVMRevertibleRandomFunction(handler), NewVMGetBlockFunction(handler), diff --git a/stdlib/comparison.go b/stdlib/comparison.go index 6cd6e2c1aa..05fcd8f788 100644 --- a/stdlib/comparison.go +++ b/stdlib/comparison.go @@ -23,20 +23,21 @@ import ( "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" ) -// MinFunction +// MinOfFunction -const minFunctionName = "min" +const minOfFunctionName = "minOf" -const minFunctionDocString = ` +const minOfFunctionDocString = ` Returns the minimum of the two given values. The arguments must be of the same comparable type. ` -var minFunctionType = func() *sema.FunctionType { +var minOfFunctionType = func() *sema.FunctionType { typeParameter := &sema.TypeParameter{ Name: "T", // No TypeBound - we check comparability in TypeArgumentsCheck @@ -85,7 +86,7 @@ var minFunctionType = func() *sema.FunctionType { Range: ast.NewRangeFromPositioned(memoryGauge, invocationRange), Details: fmt.Sprintf( "Type argument for `%s` must be a comparable type, got `%s`", - minFunctionName, + minOfFunctionName, typeArg, ), }) @@ -94,7 +95,7 @@ var minFunctionType = func() *sema.FunctionType { } }() -var NativeMinFunction = interpreter.NativeFunction( +var NativeMinOfFunction = interpreter.NativeFunction( func( context interpreter.NativeFunctionContext, _ interpreter.TypeArgumentsIterator, @@ -107,47 +108,47 @@ var NativeMinFunction = interpreter.NativeFunction( comparableA, ok := a.(interpreter.ComparableValue) if !ok { - panic(fmt.Sprintf("min: first argument is not comparable: %T", a)) + panic(errors.NewUnreachableError()) } comparableB, ok := b.(interpreter.ComparableValue) if !ok { - panic(fmt.Sprintf("min: second argument is not comparable: %T", b)) + panic(errors.NewUnreachableError()) } - if bool(comparableA.Less(context, comparableB)) { + if comparableA.Less(context, comparableB) { return a } return b }, ) -var InterpreterMinFunction = NewNativeStandardLibraryStaticFunction( - minFunctionName, - minFunctionType, - minFunctionDocString, - NativeMinFunction, +var InterpreterMinOfFunction = NewNativeStandardLibraryStaticFunction( + minOfFunctionName, + minOfFunctionType, + minOfFunctionDocString, + NativeMinOfFunction, false, ) -var VMMinFunction = NewNativeStandardLibraryStaticFunction( - minFunctionName, - minFunctionType, - minFunctionDocString, - NativeMinFunction, +var VMMinOfFunction = NewNativeStandardLibraryStaticFunction( + minOfFunctionName, + minOfFunctionType, + minOfFunctionDocString, + NativeMinOfFunction, true, ) -// MaxFunction +// MaxOfFunction -const maxFunctionName = "max" +const maxOfFunctionName = "maxOf" -const maxFunctionDocString = ` +const maxOfFunctionDocString = ` Returns the maximum of the two given values. The arguments must be of the same comparable type. ` -var maxFunctionType = func() *sema.FunctionType { +var maxOfFunctionType = func() *sema.FunctionType { typeParameter := &sema.TypeParameter{ Name: "T", // No TypeBound - we check comparability in TypeArgumentsCheck @@ -196,7 +197,7 @@ var maxFunctionType = func() *sema.FunctionType { Range: ast.NewRangeFromPositioned(memoryGauge, invocationRange), Details: fmt.Sprintf( "Type argument for `%s` must be a comparable type, got `%s`", - maxFunctionName, + maxOfFunctionName, typeArg, ), }) @@ -205,7 +206,7 @@ var maxFunctionType = func() *sema.FunctionType { } }() -var NativeMaxFunction = interpreter.NativeFunction( +var NativeMaxOfFunction = interpreter.NativeFunction( func( context interpreter.NativeFunctionContext, _ interpreter.TypeArgumentsIterator, @@ -233,18 +234,18 @@ var NativeMaxFunction = interpreter.NativeFunction( }, ) -var InterpreterMaxFunction = NewNativeStandardLibraryStaticFunction( - maxFunctionName, - maxFunctionType, - maxFunctionDocString, - NativeMaxFunction, +var InterpreterMaxOfFunction = NewNativeStandardLibraryStaticFunction( + maxOfFunctionName, + maxOfFunctionType, + maxOfFunctionDocString, + NativeMaxOfFunction, false, ) -var VMMaxFunction = NewNativeStandardLibraryStaticFunction( - maxFunctionName, - maxFunctionType, - maxFunctionDocString, - NativeMaxFunction, +var VMMaxOfFunction = NewNativeStandardLibraryStaticFunction( + maxOfFunctionName, + maxOfFunctionType, + maxOfFunctionDocString, + NativeMaxOfFunction, true, ) diff --git a/stdlib/comparison_test.go b/stdlib/comparison_test.go index f1e6631fb6..97901d2661 100644 --- a/stdlib/comparison_test.go +++ b/stdlib/comparison_test.go @@ -30,11 +30,11 @@ import ( . "github.com/onflow/cadence/test_utils/sema_utils" ) -func TestMinFunction(t *testing.T) { +func TestMinOfFunction(t *testing.T) { t.Parallel() baseValueActivation := sema.NewVariableActivation(sema.BaseValueActivation) - baseValueActivation.DeclareValue(InterpreterMinFunction) + baseValueActivation.DeclareValue(InterpreterMinOfFunction) parseAndCheck := func(t *testing.T, code string) (*sema.Checker, error) { return ParseAndCheckWithOptions(t, @@ -53,7 +53,7 @@ func TestMinFunction(t *testing.T) { t.Parallel() _, err := parseAndCheck(t, ` - let result = min(5, 10) + let result = minOf(5, 10) `) require.NoError(t, err) @@ -63,7 +63,7 @@ func TestMinFunction(t *testing.T) { t.Parallel() _, err := parseAndCheck(t, ` - let result = min(5, 10) + let result = minOf(5, 10) `) require.NoError(t, err) @@ -73,7 +73,7 @@ func TestMinFunction(t *testing.T) { t.Parallel() _, err := parseAndCheck(t, ` - let result = min(5.5, 10.5) + let result = minOf(5.5, 10.5) `) require.NoError(t, err) @@ -83,7 +83,7 @@ func TestMinFunction(t *testing.T) { t.Parallel() _, err := parseAndCheck(t, ` - let result = min("a", "b") + let result = minOf("a", "b") `) require.NoError(t, err) @@ -95,7 +95,7 @@ func TestMinFunction(t *testing.T) { _, err := parseAndCheck(t, ` fun foo(): Void {} fun bar(): Void {} - let result = min(foo, bar) + let result = minOf(foo, bar) `) errs := RequireCheckerErrors(t, err, 1) @@ -106,7 +106,7 @@ func TestMinFunction(t *testing.T) { t.Parallel() _, err := parseAndCheck(t, ` - let result = min(5, 10.5) + let result = minOf(5, 10.5) `) errs := RequireCheckerErrors(t, err, 1) @@ -114,11 +114,11 @@ func TestMinFunction(t *testing.T) { }) } -func TestMaxFunction(t *testing.T) { +func TestMaxOfFunction(t *testing.T) { t.Parallel() baseValueActivation := sema.NewVariableActivation(sema.BaseValueActivation) - baseValueActivation.DeclareValue(InterpreterMaxFunction) + baseValueActivation.DeclareValue(InterpreterMaxOfFunction) parseAndCheck := func(t *testing.T, code string) (*sema.Checker, error) { return ParseAndCheckWithOptions(t, @@ -137,7 +137,7 @@ func TestMaxFunction(t *testing.T) { t.Parallel() _, err := parseAndCheck(t, ` - let result = max(5, 10) + let result = maxOf(5, 10) `) require.NoError(t, err) @@ -147,7 +147,7 @@ func TestMaxFunction(t *testing.T) { t.Parallel() _, err := parseAndCheck(t, ` - let result = max(5, 10) + let result = maxOf(5, 10) `) require.NoError(t, err) @@ -157,7 +157,7 @@ func TestMaxFunction(t *testing.T) { t.Parallel() _, err := parseAndCheck(t, ` - let result = max(5.5, 10.5) + let result = maxOf(5.5, 10.5) `) require.NoError(t, err) @@ -167,7 +167,7 @@ func TestMaxFunction(t *testing.T) { t.Parallel() _, err := parseAndCheck(t, ` - let result = max("a", "b") + let result = maxOf("a", "b") `) require.NoError(t, err) @@ -177,7 +177,7 @@ func TestMaxFunction(t *testing.T) { t.Parallel() _, err := parseAndCheck(t, ` - let result = max<{String: Int}>({}, {}) + let result = maxOf<{String: Int}>({}, {}) `) errs := RequireCheckerErrors(t, err, 1) @@ -188,7 +188,7 @@ func TestMaxFunction(t *testing.T) { t.Parallel() _, err := parseAndCheck(t, ` - let result = max(5.5, 10) + let result = maxOf(5.5, 10) `) errs := RequireCheckerErrors(t, err, 1) @@ -196,37 +196,37 @@ func TestMaxFunction(t *testing.T) { }) } -func TestMinFunctionRuntime(t *testing.T) { +func TestMinOfFunctionRuntime(t *testing.T) { t.Parallel() - t.Run("Int min", func(t *testing.T) { + t.Run("Int", func(t *testing.T) { t.Parallel() inter := newInterpreter(t, ` - access(all) let result = min(5, 10) - `, InterpreterMinFunction) + access(all) let result = minOf(5, 10) + `, InterpreterMinOfFunction) result := inter.Globals.Get("result").GetValue(inter) require.Equal(t, interpreter.NewUnmeteredIntValueFromInt64(5), result) }) - t.Run("Int max argument", func(t *testing.T) { + t.Run("Int, reversed", func(t *testing.T) { t.Parallel() inter := newInterpreter(t, ` - access(all) let result = min(10, 5) - `, InterpreterMinFunction) + access(all) let result = minOf(10, 5) + `, InterpreterMinOfFunction) result := inter.Globals.Get("result").GetValue(inter) require.Equal(t, interpreter.NewUnmeteredIntValueFromInt64(5), result) }) - t.Run("UFix64", func(t *testing.T) { + t.Run("UFix64, explicit type argument", func(t *testing.T) { t.Parallel() inter := newInterpreter(t, ` - access(all) let result = min(5.5, 10.5) - `, InterpreterMinFunction) + access(all) let result = minOf(5.5, 10.5) + `, InterpreterMinOfFunction) result := inter.Globals.Get("result").GetValue(inter) expected := interpreter.NewUnmeteredUFix64Value(550_000_000) @@ -234,37 +234,37 @@ func TestMinFunctionRuntime(t *testing.T) { }) } -func TestMaxFunctionRuntime(t *testing.T) { +func TestMaxOfFunctionRuntime(t *testing.T) { t.Parallel() - t.Run("Int max", func(t *testing.T) { + t.Run("Int", func(t *testing.T) { t.Parallel() inter := newInterpreter(t, ` - access(all) let result = max(5, 10) - `, InterpreterMaxFunction) + access(all) let result = maxOf(5, 10) + `, InterpreterMaxOfFunction) result := inter.Globals.Get("result").GetValue(inter) require.Equal(t, interpreter.NewUnmeteredIntValueFromInt64(10), result) }) - t.Run("Int min argument", func(t *testing.T) { + t.Run("Int, reversed", func(t *testing.T) { t.Parallel() inter := newInterpreter(t, ` - access(all) let result = max(10, 5) - `, InterpreterMaxFunction) + access(all) let result = maxOf(10, 5) + `, InterpreterMaxOfFunction) result := inter.Globals.Get("result").GetValue(inter) require.Equal(t, interpreter.NewUnmeteredIntValueFromInt64(10), result) }) - t.Run("UFix64", func(t *testing.T) { + t.Run("UFix64, explicit type argument", func(t *testing.T) { t.Parallel() inter := newInterpreter(t, ` - access(all) let result = max(5.5, 10.5) - `, InterpreterMaxFunction) + access(all) let result = maxOf(5.5, 10.5) + `, InterpreterMaxOfFunction) result := inter.Globals.Get("result").GetValue(inter) expected := interpreter.NewUnmeteredUFix64Value(1_050_000_000) From 4b87ffe11363a16966e5d703e9e7252acc1ec440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Mon, 9 Feb 2026 16:00:04 -0800 Subject: [PATCH 3/7] assert return types --- stdlib/comparison_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/stdlib/comparison_test.go b/stdlib/comparison_test.go index 97901d2661..3c00fbbe76 100644 --- a/stdlib/comparison_test.go +++ b/stdlib/comparison_test.go @@ -53,7 +53,7 @@ func TestMinOfFunction(t *testing.T) { t.Parallel() _, err := parseAndCheck(t, ` - let result = minOf(5, 10) + let result: Int = minOf(5, 10) `) require.NoError(t, err) @@ -63,7 +63,7 @@ func TestMinOfFunction(t *testing.T) { t.Parallel() _, err := parseAndCheck(t, ` - let result = minOf(5, 10) + let result: Int8 = minOf(5, 10) `) require.NoError(t, err) @@ -73,7 +73,7 @@ func TestMinOfFunction(t *testing.T) { t.Parallel() _, err := parseAndCheck(t, ` - let result = minOf(5.5, 10.5) + let result: UFix64 = minOf(5.5, 10.5) `) require.NoError(t, err) @@ -83,7 +83,7 @@ func TestMinOfFunction(t *testing.T) { t.Parallel() _, err := parseAndCheck(t, ` - let result = minOf("a", "b") + let result: String = minOf("a", "b") `) require.NoError(t, err) @@ -137,7 +137,7 @@ func TestMaxOfFunction(t *testing.T) { t.Parallel() _, err := parseAndCheck(t, ` - let result = maxOf(5, 10) + let result: Int = maxOf(5, 10) `) require.NoError(t, err) @@ -147,7 +147,7 @@ func TestMaxOfFunction(t *testing.T) { t.Parallel() _, err := parseAndCheck(t, ` - let result = maxOf(5, 10) + let result: Int16 = maxOf(5, 10) `) require.NoError(t, err) @@ -157,7 +157,7 @@ func TestMaxOfFunction(t *testing.T) { t.Parallel() _, err := parseAndCheck(t, ` - let result = maxOf(5.5, 10.5) + let result: Fix64 = maxOf(5.5, 10.5) `) require.NoError(t, err) @@ -167,7 +167,7 @@ func TestMaxOfFunction(t *testing.T) { t.Parallel() _, err := parseAndCheck(t, ` - let result = maxOf("a", "b") + let result: String = maxOf("a", "b") `) require.NoError(t, err) From f1a5561514eb2a867393ef4292ba72c416a09813 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Tue, 17 Feb 2026 14:51:46 -0800 Subject: [PATCH 4/7] rename to min/max, define in Comparison location --- stdlib/builtin.go | 4 - stdlib/comparison.go | 96 ++++++++++--------- stdlib/comparison_test.go | 192 ++++++++++++++++++++++++++++--------- tools/analysis/programs.go | 2 + 4 files changed, 202 insertions(+), 92 deletions(-) diff --git a/stdlib/builtin.go b/stdlib/builtin.go index 570a338187..76d39f9ef5 100644 --- a/stdlib/builtin.go +++ b/stdlib/builtin.go @@ -58,8 +58,6 @@ func InterpreterDefaultStandardLibraryValues(handler StandardLibraryHandler) []S InterpreterPanicFunction, InterpreterSignatureAlgorithmConstructor, InterpreterInclusiveRangeConstructor, - InterpreterMinOfFunction, - InterpreterMaxOfFunction, NewInterpreterLogFunction(handler), NewInterpreterRevertibleRandomFunction(handler), NewInterpreterGetBlockFunction(handler), @@ -79,8 +77,6 @@ func VMDefaultStandardLibraryValues(handler StandardLibraryHandler) []StandardLi VMPanicFunction, VMSignatureAlgorithmConstructor, VMInclusiveRangeConstructor, - VMMinOfFunction, - VMMaxOfFunction, NewVMLogFunction(handler), NewVMRevertibleRandomFunction(handler), NewVMGetBlockFunction(handler), diff --git a/stdlib/comparison.go b/stdlib/comparison.go index 05fcd8f788..2cac81958c 100644 --- a/stdlib/comparison.go +++ b/stdlib/comparison.go @@ -28,16 +28,56 @@ import ( "github.com/onflow/cadence/sema" ) -// MinOfFunction +const ComparisonContractLocation = common.IdentifierLocation("Comparison") -const minOfFunctionName = "minOf" +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), + }) + elements.Set(maxFunctionName, sema.ImportElement{ + Type: maxFunctionType, + DeclarationKind: common.DeclarationKindFunction, + Access: sema.PrimitiveAccess(ast.AccessAll), + }) + return elements + }(), +} -const minOfFunctionDocString = ` +var ComparisonContractInterpreterImport = interpreter.VirtualImport{ + Globals: []interpreter.VirtualImportGlobal{ + { + Name: minFunctionName, + Value: interpreter.NewStaticHostFunctionValueFromNativeFunction( + nil, + minFunctionType, + NativeMinFunction, + ), + }, + { + Name: maxFunctionName, + Value: interpreter.NewStaticHostFunctionValueFromNativeFunction( + nil, + maxFunctionType, + NativeMaxFunction, + ), + }, + }, +} + +// MinFunction + +const minFunctionName = "min" + +const minFunctionDocString = ` Returns the minimum of the two given values. The arguments must be of the same comparable type. ` -var minOfFunctionType = func() *sema.FunctionType { +var minFunctionType = func() *sema.FunctionType { typeParameter := &sema.TypeParameter{ Name: "T", // No TypeBound - we check comparability in TypeArgumentsCheck @@ -86,7 +126,7 @@ var minOfFunctionType = func() *sema.FunctionType { Range: ast.NewRangeFromPositioned(memoryGauge, invocationRange), Details: fmt.Sprintf( "Type argument for `%s` must be a comparable type, got `%s`", - minOfFunctionName, + minFunctionName, typeArg, ), }) @@ -95,7 +135,7 @@ var minOfFunctionType = func() *sema.FunctionType { } }() -var NativeMinOfFunction = interpreter.NativeFunction( +var NativeMinFunction = interpreter.NativeFunction( func( context interpreter.NativeFunctionContext, _ interpreter.TypeArgumentsIterator, @@ -123,32 +163,16 @@ var NativeMinOfFunction = interpreter.NativeFunction( }, ) -var InterpreterMinOfFunction = NewNativeStandardLibraryStaticFunction( - minOfFunctionName, - minOfFunctionType, - minOfFunctionDocString, - NativeMinOfFunction, - false, -) - -var VMMinOfFunction = NewNativeStandardLibraryStaticFunction( - minOfFunctionName, - minOfFunctionType, - minOfFunctionDocString, - NativeMinOfFunction, - true, -) - -// MaxOfFunction +// MaxFunction -const maxOfFunctionName = "maxOf" +const maxFunctionName = "max" -const maxOfFunctionDocString = ` +const maxFunctionDocString = ` Returns the maximum of the two given values. The arguments must be of the same comparable type. ` -var maxOfFunctionType = func() *sema.FunctionType { +var maxFunctionType = func() *sema.FunctionType { typeParameter := &sema.TypeParameter{ Name: "T", // No TypeBound - we check comparability in TypeArgumentsCheck @@ -197,7 +221,7 @@ var maxOfFunctionType = func() *sema.FunctionType { Range: ast.NewRangeFromPositioned(memoryGauge, invocationRange), Details: fmt.Sprintf( "Type argument for `%s` must be a comparable type, got `%s`", - maxOfFunctionName, + maxFunctionName, typeArg, ), }) @@ -206,7 +230,7 @@ var maxOfFunctionType = func() *sema.FunctionType { } }() -var NativeMaxOfFunction = interpreter.NativeFunction( +var NativeMaxFunction = interpreter.NativeFunction( func( context interpreter.NativeFunctionContext, _ interpreter.TypeArgumentsIterator, @@ -233,19 +257,3 @@ var NativeMaxOfFunction = interpreter.NativeFunction( return b }, ) - -var InterpreterMaxOfFunction = NewNativeStandardLibraryStaticFunction( - maxOfFunctionName, - maxOfFunctionType, - maxOfFunctionDocString, - NativeMaxOfFunction, - false, -) - -var VMMaxOfFunction = NewNativeStandardLibraryStaticFunction( - maxOfFunctionName, - maxOfFunctionType, - maxOfFunctionDocString, - NativeMaxOfFunction, - true, -) diff --git a/stdlib/comparison_test.go b/stdlib/comparison_test.go index 3c00fbbe76..d7ba1066c1 100644 --- a/stdlib/comparison_test.go +++ b/stdlib/comparison_test.go @@ -19,30 +19,39 @@ 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 TestMinOfFunction(t *testing.T) { +func TestMinFunction(t *testing.T) { t.Parallel() - baseValueActivation := sema.NewVariableActivation(sema.BaseValueActivation) - baseValueActivation.DeclareValue(InterpreterMinOfFunction) - parseAndCheck := func(t *testing.T, code string) (*sema.Checker, error) { return ParseAndCheckWithOptions(t, code, ParseAndCheckOptions{ CheckerConfig: &sema.Config{ - BaseValueActivationHandler: func(_ common.Location) *sema.VariableActivation { - return baseValueActivation + 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) }, }, }, @@ -53,7 +62,9 @@ func TestMinOfFunction(t *testing.T) { t.Parallel() _, err := parseAndCheck(t, ` - let result: Int = minOf(5, 10) + import Comparison + + let result: Int = min(5, 10) `) require.NoError(t, err) @@ -63,7 +74,9 @@ func TestMinOfFunction(t *testing.T) { t.Parallel() _, err := parseAndCheck(t, ` - let result: Int8 = minOf(5, 10) + import Comparison + + let result: Int8 = min(5, 10) `) require.NoError(t, err) @@ -73,7 +86,9 @@ func TestMinOfFunction(t *testing.T) { t.Parallel() _, err := parseAndCheck(t, ` - let result: UFix64 = minOf(5.5, 10.5) + import Comparison + + let result: UFix64 = min(5.5, 10.5) `) require.NoError(t, err) @@ -83,7 +98,9 @@ func TestMinOfFunction(t *testing.T) { t.Parallel() _, err := parseAndCheck(t, ` - let result: String = minOf("a", "b") + import Comparison + + let result: String = min("a", "b") `) require.NoError(t, err) @@ -93,9 +110,11 @@ func TestMinOfFunction(t *testing.T) { t.Parallel() _, err := parseAndCheck(t, ` + import Comparison + fun foo(): Void {} fun bar(): Void {} - let result = minOf(foo, bar) + let result = min(foo, bar) `) errs := RequireCheckerErrors(t, err, 1) @@ -106,7 +125,9 @@ func TestMinOfFunction(t *testing.T) { t.Parallel() _, err := parseAndCheck(t, ` - let result = minOf(5, 10.5) + import Comparison + + let result = min(5, 10.5) `) errs := RequireCheckerErrors(t, err, 1) @@ -114,19 +135,23 @@ func TestMinOfFunction(t *testing.T) { }) } -func TestMaxOfFunction(t *testing.T) { +func TestMaxFunction(t *testing.T) { t.Parallel() - baseValueActivation := sema.NewVariableActivation(sema.BaseValueActivation) - baseValueActivation.DeclareValue(InterpreterMaxOfFunction) - parseAndCheck := func(t *testing.T, code string) (*sema.Checker, error) { return ParseAndCheckWithOptions(t, code, ParseAndCheckOptions{ CheckerConfig: &sema.Config{ - BaseValueActivationHandler: func(_ common.Location) *sema.VariableActivation { - return baseValueActivation + 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) }, }, }, @@ -137,7 +162,9 @@ func TestMaxOfFunction(t *testing.T) { t.Parallel() _, err := parseAndCheck(t, ` - let result: Int = maxOf(5, 10) + import Comparison + + let result: Int = max(5, 10) `) require.NoError(t, err) @@ -147,7 +174,9 @@ func TestMaxOfFunction(t *testing.T) { t.Parallel() _, err := parseAndCheck(t, ` - let result: Int16 = maxOf(5, 10) + import Comparison + + let result: Int16 = max(5, 10) `) require.NoError(t, err) @@ -157,7 +186,9 @@ func TestMaxOfFunction(t *testing.T) { t.Parallel() _, err := parseAndCheck(t, ` - let result: Fix64 = maxOf(5.5, 10.5) + import Comparison + + let result: Fix64 = max(5.5, 10.5) `) require.NoError(t, err) @@ -167,7 +198,9 @@ func TestMaxOfFunction(t *testing.T) { t.Parallel() _, err := parseAndCheck(t, ` - let result: String = maxOf("a", "b") + import Comparison + + let result: String = max("a", "b") `) require.NoError(t, err) @@ -177,7 +210,9 @@ func TestMaxOfFunction(t *testing.T) { t.Parallel() _, err := parseAndCheck(t, ` - let result = maxOf<{String: Int}>({}, {}) + import Comparison + + let result = max<{String: Int}>({}, {}) `) errs := RequireCheckerErrors(t, err, 1) @@ -188,7 +223,9 @@ func TestMaxOfFunction(t *testing.T) { t.Parallel() _, err := parseAndCheck(t, ` - let result = maxOf(5.5, 10) + import Comparison + + let result = max(5.5, 10) `) errs := RequireCheckerErrors(t, err, 1) @@ -196,15 +233,72 @@ func TestMaxOfFunction(t *testing.T) { }) } -func TestMinOfFunctionRuntime(t *testing.T) { +// 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 := newInterpreter(t, ` - access(all) let result = minOf(5, 10) - `, InterpreterMinOfFunction) + 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) @@ -213,9 +307,11 @@ func TestMinOfFunctionRuntime(t *testing.T) { t.Run("Int, reversed", func(t *testing.T) { t.Parallel() - inter := newInterpreter(t, ` - access(all) let result = minOf(10, 5) - `, InterpreterMinOfFunction) + 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) @@ -224,9 +320,11 @@ func TestMinOfFunctionRuntime(t *testing.T) { t.Run("UFix64, explicit type argument", func(t *testing.T) { t.Parallel() - inter := newInterpreter(t, ` - access(all) let result = minOf(5.5, 10.5) - `, InterpreterMinOfFunction) + 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) @@ -234,15 +332,17 @@ func TestMinOfFunctionRuntime(t *testing.T) { }) } -func TestMaxOfFunctionRuntime(t *testing.T) { +func TestMaxFunctionRuntime(t *testing.T) { t.Parallel() t.Run("Int", func(t *testing.T) { t.Parallel() - inter := newInterpreter(t, ` - access(all) let result = maxOf(5, 10) - `, InterpreterMaxOfFunction) + 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) @@ -251,9 +351,11 @@ func TestMaxOfFunctionRuntime(t *testing.T) { t.Run("Int, reversed", func(t *testing.T) { t.Parallel() - inter := newInterpreter(t, ` - access(all) let result = maxOf(10, 5) - `, InterpreterMaxOfFunction) + 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) @@ -262,9 +364,11 @@ func TestMaxOfFunctionRuntime(t *testing.T) { t.Run("UFix64, explicit type argument", func(t *testing.T) { t.Parallel() - inter := newInterpreter(t, ` - access(all) let result = maxOf(5.5, 10.5) - `, InterpreterMaxOfFunction) + 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) diff --git a/tools/analysis/programs.go b/tools/analysis/programs.go index c23643ba51..186477c6f3 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 From e9ae4a8d3decaf32e00ce0f65bebb1e3b1fbe40a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Tue, 17 Feb 2026 15:42:53 -0800 Subject: [PATCH 5/7] add clamp function --- stdlib/comparison.go | 129 ++++++++++++++++++++++++++- stdlib/comparison_test.go | 181 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 308 insertions(+), 2 deletions(-) diff --git a/stdlib/comparison.go b/stdlib/comparison.go index 2cac81958c..0a42f01dca 100644 --- a/stdlib/comparison.go +++ b/stdlib/comparison.go @@ -43,6 +43,11 @@ var ComparisonContractSemaImport = sema.VirtualImport{ DeclarationKind: common.DeclarationKindFunction, Access: sema.PrimitiveAccess(ast.AccessAll), }) + elements.Set(clampFunctionName, sema.ImportElement{ + Type: clampFunctionType, + DeclarationKind: common.DeclarationKindFunction, + Access: sema.PrimitiveAccess(ast.AccessAll), + }) return elements }(), } @@ -65,6 +70,14 @@ var ComparisonContractInterpreterImport = interpreter.VirtualImport{ NativeMaxFunction, ), }, + { + Name: clampFunctionName, + Value: interpreter.NewStaticHostFunctionValueFromNativeFunction( + nil, + clampFunctionType, + NativeClampFunction, + ), + }, }, } @@ -243,12 +256,12 @@ var NativeMaxFunction = interpreter.NativeFunction( comparableA, ok := a.(interpreter.ComparableValue) if !ok { - panic(fmt.Sprintf("max: first argument is not comparable: %T", a)) + panic(errors.NewUnreachableError()) } comparableB, ok := b.(interpreter.ComparableValue) if !ok { - panic(fmt.Sprintf("max: second argument is not comparable: %T", b)) + panic(errors.NewUnreachableError()) } if bool(comparableA.Greater(context, comparableB)) { @@ -257,3 +270,115 @@ var NativeMaxFunction = interpreter.NativeFunction( 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 index d7ba1066c1..46d2e84ea3 100644 --- a/stdlib/comparison_test.go +++ b/stdlib/comparison_test.go @@ -375,3 +375,184 @@ func TestMaxFunctionRuntime(t *testing.T) { 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) + }) +} From 3089b79b8b3130d58f8a5f62aafc72f45c7bfe44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Fri, 20 Feb 2026 10:59:07 -0800 Subject: [PATCH 6/7] remove unnecessary cast Co-authored-by: Raymond Zhang --- stdlib/comparison.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stdlib/comparison.go b/stdlib/comparison.go index 0a42f01dca..6e4778f34d 100644 --- a/stdlib/comparison.go +++ b/stdlib/comparison.go @@ -264,7 +264,7 @@ var NativeMaxFunction = interpreter.NativeFunction( panic(errors.NewUnreachableError()) } - if bool(comparableA.Greater(context, comparableB)) { + if comparableA.Greater(context, comparableB) { return a } return b From 15fe57824e4442884ab4a3da414332b63c708a13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Tue, 10 Mar 2026 13:52:56 -0700 Subject: [PATCH 7/7] add docstrings --- sema/check_import_declaration.go | 1 + sema/import.go | 2 ++ stdlib/comparison.go | 42 ++++++++++++++++++++------------ 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/sema/check_import_declaration.go b/sema/check_import_declaration.go index dbb3ce8aea..251c1fec38 100644 --- a/sema/check_import_declaration.go +++ b/sema/check_import_declaration.go @@ -457,6 +457,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 4c555a2786..54a03038da 100644 --- a/sema/import.go +++ b/sema/import.go @@ -36,6 +36,7 @@ type ImportElement struct { ArgumentLabels []string DeclarationKind common.DeclarationKind Access Access + DocString string } // ElaborationImport @@ -54,6 +55,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 index 6e4778f34d..95534b2a7f 100644 --- a/stdlib/comparison.go +++ b/stdlib/comparison.go @@ -33,21 +33,33 @@ 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), - }) - elements.Set(maxFunctionName, sema.ImportElement{ - Type: maxFunctionType, - DeclarationKind: common.DeclarationKindFunction, - Access: sema.PrimitiveAccess(ast.AccessAll), - }) - elements.Set(clampFunctionName, sema.ImportElement{ - Type: clampFunctionType, - DeclarationKind: common.DeclarationKindFunction, - Access: sema.PrimitiveAccess(ast.AccessAll), - }) + 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 }(), }