diff --git a/bbq/vm/test/vm_test.go b/bbq/vm/test/vm_test.go index 0a818958b..5c54c290b 100644 --- a/bbq/vm/test/vm_test.go +++ b/bbq/vm/test/vm_test.go @@ -9512,6 +9512,43 @@ func TestGetAuthAccount(t *testing.T) { }) } +func TestBoolToString(t *testing.T) { + + t.Parallel() + + t.Run("true", func(t *testing.T) { + t.Parallel() + + result, err := CompileAndInvoke(t, + ` + fun test(): String { + let x: Bool = true + return x.toString() + } + `, + "test", + ) + require.NoError(t, err) + require.Equal(t, interpreter.NewUnmeteredStringValue("true"), result) + }) + + t.Run("false", func(t *testing.T) { + t.Parallel() + + result, err := CompileAndInvoke(t, + ` + fun test(): String { + let x: Bool = false + return x.toString() + } + `, + "test", + ) + require.NoError(t, err) + require.Equal(t, interpreter.NewUnmeteredStringValue("false"), result) + }) +} + func TestStringTemplate(t *testing.T) { t.Parallel() @@ -9550,6 +9587,23 @@ func TestStringTemplate(t *testing.T) { require.NoError(t, err) require.Equal(t, interpreter.NewUnmeteredStringValue("A + B = 4"), result) }) + + t.Run("bool", func(t *testing.T) { + t.Parallel() + + result, err := CompileAndInvoke(t, + ` + fun test(): String { + let x = true + let y = false + return "\(x) and \(y)" + } + `, + "test", + ) + require.NoError(t, err) + require.Equal(t, interpreter.NewUnmeteredStringValue("true and false"), result) + }) } type assumeValidPublicKeyValidator struct{} diff --git a/bbq/vm/value_bool.go b/bbq/vm/value_bool.go new file mode 100644 index 000000000..892c541f1 --- /dev/null +++ b/bbq/vm/value_bool.go @@ -0,0 +1,41 @@ +/* + * 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 vm + +import ( + "github.com/onflow/cadence/bbq/commons" + "github.com/onflow/cadence/interpreter" + "github.com/onflow/cadence/sema" +) + +// members + +func init() { + + typeName := commons.TypeQualifier(sema.BoolType) + + registerBuiltinTypeBoundFunction( + typeName, + NewNativeFunctionValue( + sema.ToStringFunctionName, + sema.ToStringFunctionType, + interpreter.NativeBoolValueToStringFunction, + ), + ) +} diff --git a/bbq/vm/value_path.go b/bbq/vm/value_path.go index a624a9901..c46c2b5ec 100644 --- a/bbq/vm/value_path.go +++ b/bbq/vm/value_path.go @@ -28,13 +28,7 @@ import ( func init() { - for _, pathType := range []sema.Type{ - sema.PathType, - sema.StoragePathType, - sema.CapabilityPathType, - sema.PublicPathType, - sema.PrivatePathType, - } { + for _, pathType := range sema.AllPathTypes { typeName := commons.TypeQualifier(pathType) registerBuiltinTypeBoundFunction( diff --git a/interpreter/builtinfunctions_test.go b/interpreter/builtinfunctions_test.go index cca9fd4ed..4cfda04ab 100644 --- a/interpreter/builtinfunctions_test.go +++ b/interpreter/builtinfunctions_test.go @@ -82,6 +82,40 @@ func TestInterpretToString(t *testing.T) { ) }) + t.Run("Bool, true", func(t *testing.T) { + + t.Parallel() + + inter := parseCheckAndPrepare(t, ` + let x: Bool = true + let y = x.toString() + `) + + AssertValuesEqual( + t, + inter, + interpreter.NewUnmeteredStringValue("true"), + inter.GetGlobal("y"), + ) + }) + + t.Run("Bool, false", func(t *testing.T) { + + t.Parallel() + + inter := parseCheckAndPrepare(t, ` + let x: Bool = false + let y = x.toString() + `) + + AssertValuesEqual( + t, + inter, + interpreter.NewUnmeteredStringValue("false"), + inter.GetGlobal("y"), + ) + }) + for _, ty := range sema.AllFixedPointTypes { t.Run(ty.String(), func(t *testing.T) { diff --git a/interpreter/value_bool.go b/interpreter/value_bool.go index 610c406dc..5d9705ed6 100644 --- a/interpreter/value_bool.go +++ b/interpreter/value_bool.go @@ -34,6 +34,7 @@ type BoolValue values.BoolValue var _ Value = BoolValue(false) var _ EquatableValue = BoolValue(false) var _ HashableValue = BoolValue(false) +var _ MemberAccessibleValue = BoolValue(false) const TrueValue = BoolValue(true) const FalseValue = BoolValue(false) @@ -191,3 +192,55 @@ func (v BoolValue) Clone(_ ValueCloneContext) Value { func (BoolValue) DeepRemove(_ ValueRemoveContext, _ bool) { // NO-OP } + +func (v BoolValue) GetMember(context MemberAccessibleContext, name string, memberKind common.DeclarationKind) Value { + return GetMember( + context, + v, + name, + memberKind, + nil, + ) +} + +func (v BoolValue) GetMethod(context MemberAccessibleContext, name string) FunctionValue { + switch name { + case sema.ToStringFunctionName: + return NewBoundHostFunctionValue( + context, + v, + sema.ToStringFunctionType, + NativeBoolValueToStringFunction, + ) + } + + return nil +} + +var TrueStringValue = NewUnmeteredStringValue("true") +var FalseStringValue = NewUnmeteredStringValue("false") + +var NativeBoolValueToStringFunction = NativeFunction( + func( + context NativeFunctionContext, + _ TypeArgumentsIterator, + _ ArgumentTypesIterator, + receiver Value, + _ []Value, + ) Value { + if AssertValueOfType[BoolValue](receiver) { + return TrueStringValue + } + return FalseStringValue + }, +) + +func (BoolValue) RemoveMember(_ ValueTransferContext, _ string) Value { + // Bool has no removable members (fields / functions) + panic(errors.NewUnreachableError()) +} + +func (BoolValue) SetMember(_ ValueTransferContext, _ string, _ Value) bool { + // Bool has no settable members (fields / functions) + panic(errors.NewUnreachableError()) +} diff --git a/sema/builtinfunctions_test.go b/sema/builtinfunctions_test.go index ad94f683c..dc16883a8 100644 --- a/sema/builtinfunctions_test.go +++ b/sema/builtinfunctions_test.go @@ -37,8 +37,11 @@ func TestCheckToString(t *testing.T) { for _, numberOrAddressType := range common.Concat( sema.AllNumberTypes, + sema.AllPathTypes, []sema.Type{ sema.TheAddressType, + sema.BoolType, + sema.CharacterType, }, ) { diff --git a/sema/check_string_template_expression.go b/sema/check_string_template_expression.go index 44aa746d0..328395ec2 100644 --- a/sema/check_string_template_expression.go +++ b/sema/check_string_template_expression.go @@ -46,11 +46,13 @@ func (checker *Checker) VisitStringTemplateExpression(stringTemplateExpression * for _, element := range stringTemplateExpression.Expressions { valueType := checker.VisitExpression(element, stringTemplateExpression, elementType) - if !isValidStringTemplateValue(valueType) { + if !valueType.IsInvalidType() && + !isValidStringTemplateValue(valueType) { + checker.report( &TypeMismatchWithDescriptionError{ ActualType: valueType, - ExpectedTypeDescription: "a type with built-in toString() or bool", + ExpectedTypeDescription: "a built-in type with toString()", Range: ast.NewRangeFromPositioned(checker.memoryGauge, element), }, ) diff --git a/sema/string_test.go b/sema/string_test.go index 578f81d19..6d8b97caa 100644 --- a/sema/string_test.go +++ b/sema/string_test.go @@ -727,6 +727,18 @@ func TestCheckStringTemplate(t *testing.T) { require.NoError(t, err) }) + t.Run("valid, bool", func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, ` + let a = true + let x: String = "The value of a is: \(a)" + `) + + require.NoError(t, err) + }) + t.Run("invalid, struct", func(t *testing.T) { t.Parallel() @@ -785,10 +797,9 @@ func TestCheckStringTemplate(t *testing.T) { let x: String = "\(a)" `) - errs := RequireCheckerErrors(t, err, 2) + errs := RequireCheckerErrors(t, err, 1) assert.IsType(t, &sema.NotDeclaredError{}, errs[0]) - assert.IsType(t, &sema.TypeMismatchWithDescriptionError{}, errs[1]) }) t.Run("invalid, resource", func(t *testing.T) { diff --git a/sema/type.go b/sema/type.go index 45bf8a58a..5e8dd337c 100644 --- a/sema/type.go +++ b/sema/type.go @@ -808,7 +808,8 @@ func withBuiltinMembers(ty Type, members map[string]MemberResolver) map[string]M func HasToStringFunction(ty Type) bool { switch ty { - case CharacterType: + case BoolType, + CharacterType: return true default: return IsSubType(ty, NumberType) || @@ -4712,6 +4713,14 @@ var AllNumberTypes = common.Concat( }, ) +var AllPathTypes = []Type{ + PathType, + StoragePathType, + CapabilityPathType, + PublicPathType, + PrivatePathType, +} + var BuiltinEntitlements = map[string]*EntitlementType{} var BuiltinEntitlementMappings = map[string]*EntitlementMapType{