From 4fa44fcabdd7a785fdd291c097fb5d2acdcc821d Mon Sep 17 00:00:00 2001 From: Supun Setunga Date: Thu, 13 Feb 2025 12:08:48 -0800 Subject: [PATCH 1/4] Support casting in VM --- bbq/vm/errors.go | 18 +++++++++ bbq/vm/test/vm_test.go | 92 ++++++++++++++++++++++++++++++++++++++++++ bbq/vm/types.go | 12 ++++++ bbq/vm/value.go | 82 +++++++++++++++++++++++++++++++++++++ bbq/vm/value_some.go | 2 +- bbq/vm/vm.go | 77 +++++++++++++++++++++++++++++++++-- 6 files changed, 278 insertions(+), 5 deletions(-) diff --git a/bbq/vm/errors.go b/bbq/vm/errors.go index 795941170..99a0ff5af 100644 --- a/bbq/vm/errors.go +++ b/bbq/vm/errors.go @@ -53,3 +53,21 @@ func (l MissingMemberValueError) Error() string { return fmt.Sprintf("cannot find member: `%s` in `%T`", l.Name, l.Parent) } + +// ForceCastTypeMismatchError +type ForceCastTypeMismatchError struct { + ExpectedType StaticType + ActualType StaticType +} + +var _ errors.UserError = ForceCastTypeMismatchError{} + +func (ForceCastTypeMismatchError) IsUserError() {} + +func (e ForceCastTypeMismatchError) Error() string { + return fmt.Sprintf( + "failed to force-cast value: expected type `%s`, got `%s`", + e.ExpectedType.ID(), + e.ActualType.ID(), + ) +} diff --git a/bbq/vm/test/vm_test.go b/bbq/vm/test/vm_test.go index ebcb17060..20be2549d 100644 --- a/bbq/vm/test/vm_test.go +++ b/bbq/vm/test/vm_test.go @@ -3645,3 +3645,95 @@ func TestDefaultFunctionsWithConditions(t *testing.T) { ) }) } + +func TestCasting(t *testing.T) { + + t.Parallel() + + t.Run("simple cast success", func(t *testing.T) { + t.Parallel() + + result, err := compileAndInvoke(t, + ` + fun test(x: Int): AnyStruct { + return x as Int? + } + `, + "test", + vm.NewIntValue(2), + ) + require.NoError(t, err) + assert.Equal(t, vm.NewSomeValueNonCopying(vm.NewIntValue(2)), result) + }) + + t.Run("force cast success", func(t *testing.T) { + t.Parallel() + + result, err := compileAndInvoke(t, + ` + fun test(x: AnyStruct): Int { + return x as! Int + } + `, + "test", + vm.NewIntValue(2), + ) + require.NoError(t, err) + assert.Equal(t, vm.NewIntValue(2), result) + }) + + t.Run("force cast fail", func(t *testing.T) { + t.Parallel() + + _, err := compileAndInvoke(t, + ` + fun test(x: AnyStruct): Int { + return x as! Int + } + `, + "test", + vm.BoolValue(true), + ) + require.Error(t, err) + assert.ErrorIs( + t, + err, + vm.ForceCastTypeMismatchError{ + ExpectedType: interpreter.PrimitiveStaticTypeInt, + ActualType: interpreter.PrimitiveStaticTypeBool, + }, + ) + }) + + t.Run("failable cast success", func(t *testing.T) { + t.Parallel() + + result, err := compileAndInvoke(t, + ` + fun test(x: AnyStruct): Int? { + return x as? Int + } + `, + "test", + vm.NewIntValue(2), + ) + require.NoError(t, err) + assert.Equal(t, vm.NewSomeValueNonCopying(vm.NewIntValue(2)), result) + }) + + t.Run("failable cast fail", func(t *testing.T) { + t.Parallel() + + result, err := compileAndInvoke(t, + ` + fun test(x: AnyStruct): Int? { + return x as? Int + } + `, + "test", + vm.BoolValue(true), + ) + require.NoError(t, err) + assert.Equal(t, vm.Nil, result) + }) +} diff --git a/bbq/vm/types.go b/bbq/vm/types.go index 3af1f2376..34d72a537 100644 --- a/bbq/vm/types.go +++ b/bbq/vm/types.go @@ -27,3 +27,15 @@ func IsSubType(config *Config, sourceType, targetType StaticType) bool { inter := config.interpreter() return inter.IsSubType(sourceType, targetType) } + +// UnwrapOptionalType returns the type if it is not an optional type, +// or the inner-most type if it is (optional types are repeatedly unwrapped) +func UnwrapOptionalType(ty StaticType) StaticType { + for { + optionalType, ok := ty.(*interpreter.OptionalStaticType) + if !ok { + return ty + } + ty = optionalType.Type + } +} diff --git a/bbq/vm/value.go b/bbq/vm/value.go index ba3cc89a2..1ef9cf328 100644 --- a/bbq/vm/value.go +++ b/bbq/vm/value.go @@ -20,6 +20,8 @@ package vm import ( "github.com/onflow/atree" + + "github.com/onflow/cadence/interpreter" ) type Value interface { @@ -55,3 +57,83 @@ type ReferenceTrackedResourceKindedValue interface { ValueID() atree.ValueID IsStaleResource() bool } + +// ConvertAndBox converts a value to a target type, and boxes in optionals and any value, if necessary +func ConvertAndBox( + value Value, + valueType, targetType StaticType, +) Value { + value = convert(value, valueType, targetType) + return BoxOptional(value, targetType) +} + +func convert(value Value, valueType, targetType StaticType) Value { + if valueType == nil { + return value + } + + unwrappedTargetType := UnwrapOptionalType(targetType) + + // if the value is optional, convert the inner value to the unwrapped target type + if optionalValueType, valueIsOptional := valueType.(*interpreter.OptionalStaticType); valueIsOptional { + switch value := value.(type) { + case NilValue: + return value + + case *SomeValue: + if !optionalValueType.Type.Equal(unwrappedTargetType) { + innerValue := convert(value.value, optionalValueType.Type, unwrappedTargetType) + return NewSomeValueNonCopying(innerValue) + } + return value + } + } + + switch unwrappedTargetType { + // TODO: add other cases + default: + return value + } +} + +func Unbox(value Value) Value { + for { + some, ok := value.(*SomeValue) + if !ok { + return value + } + + value = some.value + } +} + +// BoxOptional boxes a value in optionals, if necessary +func BoxOptional( + value Value, + targetType StaticType, +) Value { + + inner := value + + for { + optionalType, ok := targetType.(*interpreter.OptionalStaticType) + if !ok { + break + } + + switch typedInner := inner.(type) { + case *SomeValue: + inner = typedInner.value + + case NilValue: + // NOTE: nested nil will be unboxed! + return inner + + default: + value = NewSomeValueNonCopying(value) + } + + targetType = optionalType.Type + } + return value +} diff --git a/bbq/vm/value_some.go b/bbq/vm/value_some.go index 041d3bc80..2e52bcdc6 100644 --- a/bbq/vm/value_some.go +++ b/bbq/vm/value_some.go @@ -46,7 +46,7 @@ func (v *SomeValue) StaticType(config *Config) StaticType { return nil } return interpreter.NewOptionalStaticType( - config, + config.MemoryGauge, innerType, ) } diff --git a/bbq/vm/vm.go b/bbq/vm/vm.go index 181855abe..4ad3644c2 100644 --- a/bbq/vm/vm.go +++ b/bbq/vm/vm.go @@ -633,11 +633,80 @@ func opCast(vm *VM, ins opcode.InstructionCast) { targetType := vm.loadType(ins.TypeIndex) - // TODO: - _ = ins.Kind - _ = targetType + result := cast( + vm.config, + ins.Kind, + value, + targetType, + ) - vm.push(value) + vm.push(result) +} + +func cast(config *Config, castKind opcode.CastKind, value Value, targetType StaticType) Value { + valueType := value.StaticType(config) + + switch castKind { + case opcode.FailableCast, opcode.ForceCast: + // if the value itself has a mapped entitlement type in its authorization + // (e.g. if it is a reference to `self` or `base` in an attachment function with mapped access) + // substitution must also be performed on its entitlements + // + // we do this here (as opposed to in `IsSubTypeOfSemaType`) because casting is the only way that + // an entitlement can "traverse the boundary", so to speak, between runtime and static types, and + // thus this is the only place where it becomes necessary to "instantiate" the result of a map to its + // concrete outputs. In other places (e.g. interface conformance checks) we want to leave maps generic, + // so we don't substitute them. + + // TODO: + //valueSemaType := interpreter.SubstituteMappedEntitlements(interpreter.MustSemaTypeOfValue(value)) + //valueStaticType := ConvertSemaToStaticType(interpreter, valueSemaType) + + // If the target is anystruct or anyresource we want to preserve optionals + unboxedExpectedType := UnwrapOptionalType(targetType) + if !(unboxedExpectedType == interpreter.PrimitiveStaticTypeAnyStruct || + unboxedExpectedType == interpreter.PrimitiveStaticTypeAnyResource) { + // otherwise dynamic cast now always unboxes optionals + value = Unbox(value) + } + + isSubType := IsSubType(config, valueType, targetType) + + switch castKind { + case opcode.FailableCast: + if !isSubType { + return Nil + } + case opcode.ForceCast: + if !isSubType { + panic(ForceCastTypeMismatchError{ + ExpectedType: targetType, + ActualType: valueType, + }) + } + default: + panic(errors.NewUnreachableError()) + } + + // The failable cast may upcast to an optional type, e.g. `1 as? Int?`, so box + result := ConvertAndBox(value, valueType, targetType) + + if castKind == opcode.FailableCast { + // TODO: + // Failable casting is a resource invalidation + //interpreter.invalidateResource(value) + result = NewSomeValueNonCopying(result) + } + + return result + + case opcode.SimpleCast: + // The cast may upcast to an optional type, e.g. `1 as Int?`, so box + return ConvertAndBox(value, valueType, targetType) + + default: + panic(errors.NewUnreachableError()) + } } func opNil(vm *VM) { From c76e75a8b823d8541f43bc7a9e78930c75876dcf Mon Sep 17 00:00:00 2001 From: Supun Setunga Date: Fri, 14 Feb 2025 10:32:24 -0800 Subject: [PATCH 2/4] Split different cast variants into separate instructions --- bbq/compiler/compiler.go | 25 +++++-- bbq/opcode/castkind.go | 45 ------------ bbq/opcode/instruction.go | 10 --- bbq/opcode/instructions.go | 88 +++++++++++++++++++---- bbq/opcode/instructions.yml | 35 +++++++++- bbq/opcode/opcode.go | 7 +- bbq/opcode/opcode_string.go | 72 +++++++++---------- bbq/vm/vm.go | 134 +++++++++++++++++++----------------- 8 files changed, 237 insertions(+), 179 deletions(-) delete mode 100644 bbq/opcode/castkind.go diff --git a/bbq/compiler/compiler.go b/bbq/compiler/compiler.go index 6cacdae00..f43c99229 100644 --- a/bbq/compiler/compiler.go +++ b/bbq/compiler/compiler.go @@ -1149,14 +1149,25 @@ func (c *Compiler[_]) VisitCastingExpression(expression *ast.CastingExpression) castingTypes := c.ExtendedElaboration.CastingExpressionTypes(expression) index := c.getOrAddType(castingTypes.TargetType) - castKind := opcode.CastKindFrom(expression.Operation) - - c.codeGen.Emit( - opcode.InstructionCast{ + var castInstruction opcode.Instruction + switch expression.Operation { + case ast.OperationCast: + castInstruction = opcode.InstructionSimpleCast{ TypeIndex: index, - Kind: castKind, - }, - ) + } + case ast.OperationFailableCast: + castInstruction = opcode.InstructionFailableCast{ + TypeIndex: index, + } + case ast.OperationForceCast: + castInstruction = opcode.InstructionForceCast{ + TypeIndex: index, + } + default: + panic(errors.NewUnreachableError()) + } + + c.codeGen.Emit(castInstruction) return } diff --git a/bbq/opcode/castkind.go b/bbq/opcode/castkind.go deleted file mode 100644 index a2ec01545..000000000 --- a/bbq/opcode/castkind.go +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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 opcode - -import ( - "github.com/onflow/cadence/ast" - "github.com/onflow/cadence/errors" -) - -type CastKind byte - -const ( - SimpleCast CastKind = iota - FailableCast - ForceCast -) - -func CastKindFrom(operation ast.Operation) CastKind { - switch operation { - case ast.OperationCast: - return SimpleCast - case ast.OperationFailableCast: - return FailableCast - case ast.OperationForceCast: - return ForceCast - default: - panic(errors.NewUnreachableError()) - } -} diff --git a/bbq/opcode/instruction.go b/bbq/opcode/instruction.go index 1e0d722fa..2dd433b92 100644 --- a/bbq/opcode/instruction.go +++ b/bbq/opcode/instruction.go @@ -94,16 +94,6 @@ func emitPathDomain(code *[]byte, domain common.PathDomain) { emitByte(code, byte(domain)) } -// CastKind - -func decodeCastKind(ip *uint16, code []byte) CastKind { - return CastKind(decodeByte(ip, code)) -} - -func emitCastKind(code *[]byte, kind CastKind) { - emitByte(code, byte(kind)) -} - // CompositeKind func decodeCompositeKind(ip *uint16, code []byte) common.CompositeKind { diff --git a/bbq/opcode/instructions.go b/bbq/opcode/instructions.go index 96a7dac5b..e71b21663 100644 --- a/bbq/opcode/instructions.go +++ b/bbq/opcode/instructions.go @@ -691,37 +691,93 @@ func DecodeTransfer(ip *uint16, code []byte) (i InstructionTransfer) { return i } -// InstructionCast +// InstructionSimpleCast // // Pops a value off the stack, casts it to the given type, and then pushes it back on to the stack. -type InstructionCast struct { +type InstructionSimpleCast struct { TypeIndex uint16 - Kind CastKind } -var _ Instruction = InstructionCast{} +var _ Instruction = InstructionSimpleCast{} -func (InstructionCast) Opcode() Opcode { - return Cast +func (InstructionSimpleCast) Opcode() Opcode { + return SimpleCast } -func (i InstructionCast) String() string { +func (i InstructionSimpleCast) String() string { + var sb strings.Builder + sb.WriteString(i.Opcode().String()) + printfArgument(&sb, "typeIndex", i.TypeIndex) + return sb.String() +} + +func (i InstructionSimpleCast) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) + emitUint16(code, i.TypeIndex) +} + +func DecodeSimpleCast(ip *uint16, code []byte) (i InstructionSimpleCast) { + i.TypeIndex = decodeUint16(ip, code) + return i +} + +// InstructionFailableCast +// +// Pops a value off the stack, casts it to the given type. If the value is a subtype of the given type, then casted value is pushed back on to the stack. If the value is not a subtype of the given type, then a `nil` is pushed to the stack instead. +type InstructionFailableCast struct { + TypeIndex uint16 +} + +var _ Instruction = InstructionFailableCast{} + +func (InstructionFailableCast) Opcode() Opcode { + return FailableCast +} + +func (i InstructionFailableCast) String() string { + var sb strings.Builder + sb.WriteString(i.Opcode().String()) + printfArgument(&sb, "typeIndex", i.TypeIndex) + return sb.String() +} + +func (i InstructionFailableCast) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) + emitUint16(code, i.TypeIndex) +} + +func DecodeFailableCast(ip *uint16, code []byte) (i InstructionFailableCast) { + i.TypeIndex = decodeUint16(ip, code) + return i +} + +// InstructionForceCast +// +// Pops a value off the stack, force-casts it to the given type, and then pushes it back on to the stack. Panics if the value is not a subtype of the given type. +type InstructionForceCast struct { + TypeIndex uint16 +} + +var _ Instruction = InstructionForceCast{} + +func (InstructionForceCast) Opcode() Opcode { + return ForceCast +} + +func (i InstructionForceCast) String() string { var sb strings.Builder sb.WriteString(i.Opcode().String()) printfArgument(&sb, "typeIndex", i.TypeIndex) - printfArgument(&sb, "kind", i.Kind) return sb.String() } -func (i InstructionCast) Encode(code *[]byte) { +func (i InstructionForceCast) Encode(code *[]byte) { emitOpcode(code, i.Opcode()) emitUint16(code, i.TypeIndex) - emitCastKind(code, i.Kind) } -func DecodeCast(ip *uint16, code []byte) (i InstructionCast) { +func DecodeForceCast(ip *uint16, code []byte) (i InstructionForceCast) { i.TypeIndex = decodeUint16(ip, code) - i.Kind = decodeCastKind(ip, code) return i } @@ -1177,8 +1233,12 @@ func DecodeInstruction(ip *uint16, code []byte) Instruction { return InstructionUnwrap{} case Transfer: return DecodeTransfer(ip, code) - case Cast: - return DecodeCast(ip, code) + case SimpleCast: + return DecodeSimpleCast(ip, code) + case FailableCast: + return DecodeFailableCast(ip, code) + case ForceCast: + return DecodeForceCast(ip, code) case Jump: return DecodeJump(ip, code) case JumpIfFalse: diff --git a/bbq/opcode/instructions.yml b/bbq/opcode/instructions.yml index 87bdd8ecc..fdd97c0c3 100644 --- a/bbq/opcode/instructions.yml +++ b/bbq/opcode/instructions.yml @@ -331,14 +331,43 @@ - name: "value" type: "value" -- name: "cast" +- name: "simpleCast" description: Pops a value off the stack, casts it to the given type, and then pushes it back on to the stack. operands: - name: "typeIndex" type: "index" - - name: "kind" - type: "castKind" + valueEffects: + pop: + - name: "value" + type: "value" + push: + - name: "value" + type: "value" + +- name: "failableCast" + description: + Pops a value off the stack and casts it to the given type. + If the value is a subtype of the given type, then casted value is pushed back on to the stack. + If the value is not a subtype of the given type, then a `nil` is pushed to the stack instead. + operands: + - name: "typeIndex" + type: "index" + valueEffects: + pop: + - name: "value" + type: "value" + push: + - name: "value" + type: "optional" + +- name: "forceCast" + description: + Pops a value off the stack, force-casts it to the given type, and then pushes it back on to the stack. + Panics if the value is not a subtype of the given type. + operands: + - name: "typeIndex" + type: "index" valueEffects: pop: - name: "value" diff --git a/bbq/opcode/opcode.go b/bbq/opcode/opcode.go index d36b211ff..36746f2e6 100644 --- a/bbq/opcode/opcode.go +++ b/bbq/opcode/opcode.go @@ -72,7 +72,12 @@ const ( Unwrap Destroy Transfer - Cast + SimpleCast + FailableCast + ForceCast + _ + _ + _ _ _ _ diff --git a/bbq/opcode/opcode_string.go b/bbq/opcode/opcode_string.go index 68dd1f309..b72c41dba 100644 --- a/bbq/opcode/opcode_string.go +++ b/bbq/opcode/opcode_string.go @@ -29,29 +29,31 @@ func _() { _ = x[Unwrap-37] _ = x[Destroy-38] _ = x[Transfer-39] - _ = x[Cast-40] - _ = x[True-45] - _ = x[False-46] - _ = x[New-47] - _ = x[Path-48] - _ = x[Nil-49] - _ = x[NewArray-50] - _ = x[NewDictionary-51] - _ = x[NewRef-52] - _ = x[GetConstant-65] - _ = x[GetLocal-66] - _ = x[SetLocal-67] - _ = x[GetGlobal-68] - _ = x[SetGlobal-69] - _ = x[GetField-70] - _ = x[SetField-71] - _ = x[SetIndex-72] - _ = x[GetIndex-73] - _ = x[Invoke-85] - _ = x[InvokeDynamic-86] - _ = x[Drop-95] - _ = x[Dup-96] - _ = x[EmitEvent-103] + _ = x[SimpleCast-40] + _ = x[FailableCast-41] + _ = x[ForceCast-42] + _ = x[True-50] + _ = x[False-51] + _ = x[New-52] + _ = x[Path-53] + _ = x[Nil-54] + _ = x[NewArray-55] + _ = x[NewDictionary-56] + _ = x[NewRef-57] + _ = x[GetConstant-70] + _ = x[GetLocal-71] + _ = x[SetLocal-72] + _ = x[GetGlobal-73] + _ = x[SetGlobal-74] + _ = x[GetField-75] + _ = x[SetField-76] + _ = x[SetIndex-77] + _ = x[GetIndex-78] + _ = x[Invoke-90] + _ = x[InvokeDynamic-91] + _ = x[Drop-100] + _ = x[Dup-101] + _ = x[EmitEvent-108] } const ( @@ -59,7 +61,7 @@ const ( _Opcode_name_1 = "AddSubtractMultiplyDivideMod" _Opcode_name_2 = "LessGreaterLessOrEqualGreaterOrEqual" _Opcode_name_3 = "EqualNotEqualNot" - _Opcode_name_4 = "UnwrapDestroyTransferCast" + _Opcode_name_4 = "UnwrapDestroyTransferSimpleCastFailableCastForceCast" _Opcode_name_5 = "TrueFalseNewPathNilNewArrayNewDictionaryNewRef" _Opcode_name_6 = "GetConstantGetLocalSetLocalGetGlobalSetGlobalGetFieldSetFieldSetIndexGetIndex" _Opcode_name_7 = "InvokeInvokeDynamic" @@ -72,7 +74,7 @@ var ( _Opcode_index_1 = [...]uint8{0, 3, 11, 19, 25, 28} _Opcode_index_2 = [...]uint8{0, 4, 11, 22, 36} _Opcode_index_3 = [...]uint8{0, 5, 13, 16} - _Opcode_index_4 = [...]uint8{0, 6, 13, 21, 25} + _Opcode_index_4 = [...]uint8{0, 6, 13, 21, 31, 43, 52} _Opcode_index_5 = [...]uint8{0, 4, 9, 12, 16, 19, 27, 40, 46} _Opcode_index_6 = [...]uint8{0, 11, 19, 27, 36, 45, 53, 61, 69, 77} _Opcode_index_7 = [...]uint8{0, 6, 19} @@ -92,22 +94,22 @@ func (i Opcode) String() string { case 31 <= i && i <= 33: i -= 31 return _Opcode_name_3[_Opcode_index_3[i]:_Opcode_index_3[i+1]] - case 37 <= i && i <= 40: + case 37 <= i && i <= 42: i -= 37 return _Opcode_name_4[_Opcode_index_4[i]:_Opcode_index_4[i+1]] - case 45 <= i && i <= 52: - i -= 45 + case 50 <= i && i <= 57: + i -= 50 return _Opcode_name_5[_Opcode_index_5[i]:_Opcode_index_5[i+1]] - case 65 <= i && i <= 73: - i -= 65 + case 70 <= i && i <= 78: + i -= 70 return _Opcode_name_6[_Opcode_index_6[i]:_Opcode_index_6[i+1]] - case 85 <= i && i <= 86: - i -= 85 + case 90 <= i && i <= 91: + i -= 90 return _Opcode_name_7[_Opcode_index_7[i]:_Opcode_index_7[i+1]] - case 95 <= i && i <= 96: - i -= 95 + case 100 <= i && i <= 101: + i -= 100 return _Opcode_name_8[_Opcode_index_8[i]:_Opcode_index_8[i+1]] - case i == 103: + case i == 108: return _Opcode_name_9 default: return "Opcode(" + strconv.FormatInt(int64(i), 10) + ")" diff --git a/bbq/vm/vm.go b/bbq/vm/vm.go index 43eea1562..2710e7080 100644 --- a/bbq/vm/vm.go +++ b/bbq/vm/vm.go @@ -20,7 +20,6 @@ package vm import ( "github.com/onflow/atree" - "github.com/onflow/cadence/bbq" "github.com/onflow/cadence/bbq/commons" "github.com/onflow/cadence/bbq/constantkind" @@ -628,85 +627,88 @@ func opPath(vm *VM, ins opcode.InstructionPath) { vm.push(value) } -func opCast(vm *VM, ins opcode.InstructionCast) { +func opSimpleCast(vm *VM, ins opcode.InstructionSimpleCast) { value := vm.pop() targetType := vm.loadType(ins.TypeIndex) + valueType := value.StaticType(vm.config) - result := cast( - vm.config, - ins.Kind, - value, - targetType, - ) + // The cast may upcast to an optional type, e.g. `1 as Int?`, so box + result := ConvertAndBox(value, valueType, targetType) vm.push(result) } -func cast(config *Config, castKind opcode.CastKind, value Value, targetType StaticType) Value { - valueType := value.StaticType(config) +func opFailableCast(vm *VM, ins opcode.InstructionFailableCast) { + value := vm.pop() + + targetType := vm.loadType(ins.TypeIndex) + value, valueType := castValueAndValueType(vm.config, targetType, value) + isSubType := IsSubType(vm.config, valueType, targetType) - switch castKind { - case opcode.FailableCast, opcode.ForceCast: - // if the value itself has a mapped entitlement type in its authorization - // (e.g. if it is a reference to `self` or `base` in an attachment function with mapped access) - // substitution must also be performed on its entitlements - // - // we do this here (as opposed to in `IsSubTypeOfSemaType`) because casting is the only way that - // an entitlement can "traverse the boundary", so to speak, between runtime and static types, and - // thus this is the only place where it becomes necessary to "instantiate" the result of a map to its - // concrete outputs. In other places (e.g. interface conformance checks) we want to leave maps generic, - // so we don't substitute them. + var result Value + if isSubType { + // The failable cast may upcast to an optional type, e.g. `1 as? Int?`, so box + result = ConvertAndBox(value, valueType, targetType) // TODO: - //valueSemaType := interpreter.SubstituteMappedEntitlements(interpreter.MustSemaTypeOfValue(value)) - //valueStaticType := ConvertSemaToStaticType(interpreter, valueSemaType) - - // If the target is anystruct or anyresource we want to preserve optionals - unboxedExpectedType := UnwrapOptionalType(targetType) - if !(unboxedExpectedType == interpreter.PrimitiveStaticTypeAnyStruct || - unboxedExpectedType == interpreter.PrimitiveStaticTypeAnyResource) { - // otherwise dynamic cast now always unboxes optionals - value = Unbox(value) - } + // Failable casting is a resource invalidation + //interpreter.invalidateResource(value) - isSubType := IsSubType(config, valueType, targetType) - - switch castKind { - case opcode.FailableCast: - if !isSubType { - return Nil - } - case opcode.ForceCast: - if !isSubType { - panic(ForceCastTypeMismatchError{ - ExpectedType: targetType, - ActualType: valueType, - }) - } - default: - panic(errors.NewUnreachableError()) - } + result = NewSomeValueNonCopying(result) + } else { + result = Nil + } - // The failable cast may upcast to an optional type, e.g. `1 as? Int?`, so box - result := ConvertAndBox(value, valueType, targetType) + vm.push(result) +} - if castKind == opcode.FailableCast { - // TODO: - // Failable casting is a resource invalidation - //interpreter.invalidateResource(value) - result = NewSomeValueNonCopying(result) - } +func opForceCast(vm *VM, ins opcode.InstructionForceCast) { + value := vm.pop() - return result + targetType := vm.loadType(ins.TypeIndex) + value, valueType := castValueAndValueType(vm.config, targetType, value) + isSubType := IsSubType(vm.config, valueType, targetType) + + var result Value + if !isSubType { + panic(ForceCastTypeMismatchError{ + ExpectedType: targetType, + ActualType: valueType, + }) + } - case opcode.SimpleCast: - // The cast may upcast to an optional type, e.g. `1 as Int?`, so box - return ConvertAndBox(value, valueType, targetType) + // The force cast may upcast to an optional type, e.g. `1 as! Int?`, so box + result = ConvertAndBox(value, valueType, targetType) + vm.push(result) +} - default: - panic(errors.NewUnreachableError()) +func castValueAndValueType(config *Config, targetType StaticType, value Value) (Value, StaticType) { + valueType := value.StaticType(config) + + // if the value itself has a mapped entitlement type in its authorization + // (e.g. if it is a reference to `self` or `base` in an attachment function with mapped access) + // substitution must also be performed on its entitlements + // + // we do this here (as opposed to in `IsSubTypeOfSemaType`) because casting is the only way that + // an entitlement can "traverse the boundary", so to speak, between runtime and static types, and + // thus this is the only place where it becomes necessary to "instantiate" the result of a map to its + // concrete outputs. In other places (e.g. interface conformance checks) we want to leave maps generic, + // so we don't substitute them. + + // TODO: Substitute entitlements + //valueSemaType := interpreter.SubstituteMappedEntitlements(interpreter.MustSemaTypeOfValue(value)) + //valueType = ConvertSemaToStaticType(interpreter, valueSemaType) + + // If the target is anystruct or anyresource we want to preserve optionals + unboxedExpectedType := UnwrapOptionalType(targetType) + if !(unboxedExpectedType == interpreter.PrimitiveStaticTypeAnyStruct || + unboxedExpectedType == interpreter.PrimitiveStaticTypeAnyResource) { + // otherwise dynamic cast now always unboxes optionals + value = Unbox(value) } + + return value, valueType } func opNil(vm *VM) { @@ -861,8 +863,12 @@ func (vm *VM) run() { opDestroy(vm) case opcode.InstructionPath: opPath(vm, ins) - case opcode.InstructionCast: - opCast(vm, ins) + case opcode.InstructionSimpleCast: + opSimpleCast(vm, ins) + case opcode.InstructionFailableCast: + opFailableCast(vm, ins) + case opcode.InstructionForceCast: + opForceCast(vm, ins) case opcode.InstructionNil: opNil(vm) case opcode.InstructionEqual: From 617be19a031b9975f7b5e044fb9e50bfea76a726 Mon Sep 17 00:00:00 2001 From: Supun Setunga Date: Fri, 14 Feb 2025 10:48:20 -0800 Subject: [PATCH 3/4] Fix test --- bbq/opcode/print_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bbq/opcode/print_test.go b/bbq/opcode/print_test.go index 60f427e73..48ff3bf3a 100644 --- a/bbq/opcode/print_test.go +++ b/bbq/opcode/print_test.go @@ -101,7 +101,9 @@ func TestPrintInstruction(t *testing.T) { "New kind:CompositeKind(258) typeIndex:772": {byte(New), 1, 2, 3, 4}, - "Cast typeIndex:258 kind:3": {byte(Cast), 1, 2, 3}, + "SimpleCast typeIndex:258": {byte(SimpleCast), 1, 2, 3}, + "FailableCast typeIndex:258": {byte(FailableCast), 1, 2, 3}, + "ForceCast typeIndex:258": {byte(ForceCast), 1, 2, 3}, `Path domain:PathDomainStorage identifierIndex:5`: {byte(Path), 1, 0, 5}, From 60821225bbd2a3a9204fff0d41c9c200a69a4084 Mon Sep 17 00:00:00 2001 From: Supun Setunga Date: Fri, 14 Feb 2025 11:05:30 -0800 Subject: [PATCH 4/4] Fix lint --- bbq/opcode/instructions.go | 2 +- bbq/vm/vm.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/bbq/opcode/instructions.go b/bbq/opcode/instructions.go index e71b21663..fb088b466 100644 --- a/bbq/opcode/instructions.go +++ b/bbq/opcode/instructions.go @@ -723,7 +723,7 @@ func DecodeSimpleCast(ip *uint16, code []byte) (i InstructionSimpleCast) { // InstructionFailableCast // -// Pops a value off the stack, casts it to the given type. If the value is a subtype of the given type, then casted value is pushed back on to the stack. If the value is not a subtype of the given type, then a `nil` is pushed to the stack instead. +// Pops a value off the stack and casts it to the given type. If the value is a subtype of the given type, then casted value is pushed back on to the stack. If the value is not a subtype of the given type, then a `nil` is pushed to the stack instead. type InstructionFailableCast struct { TypeIndex uint16 } diff --git a/bbq/vm/vm.go b/bbq/vm/vm.go index 2710e7080..a736c8de2 100644 --- a/bbq/vm/vm.go +++ b/bbq/vm/vm.go @@ -20,6 +20,7 @@ package vm import ( "github.com/onflow/atree" + "github.com/onflow/cadence/bbq" "github.com/onflow/cadence/bbq/commons" "github.com/onflow/cadence/bbq/constantkind"