From 462e842fee2d13eb6107238bd295091f20d4c593 Mon Sep 17 00:00:00 2001 From: Supun Setunga Date: Fri, 14 Feb 2025 10:32:24 -0800 Subject: [PATCH] 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..292e309bc 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, 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: