diff --git a/bbq/commons/constants.go b/bbq/commons/constants.go new file mode 100644 index 0000000000..3569377da1 --- /dev/null +++ b/bbq/commons/constants.go @@ -0,0 +1,34 @@ +/* + * 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 commons + +const ( + InitFunctionName = "init" + TransactionWrapperCompositeName = "transaction" + TransactionExecuteFunctionName = "transaction.execute" + TransactionPrepareFunctionName = "transaction.prepare" + LogFunctionName = "log" + PanicFunctionName = "panic" + GetAccountFunctionName = "getAccount" + + // Names used by generated constructs + + ProgramInitFunctionName = "$_init_" + TransactionGeneratedParamPrefix = "$_param_" +) diff --git a/bbq/commons/handlers.go b/bbq/commons/handlers.go new file mode 100644 index 0000000000..b39e36352c --- /dev/null +++ b/bbq/commons/handlers.go @@ -0,0 +1,56 @@ +/* + * 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 commons + +import ( + "github.com/onflow/cadence/ast" + "github.com/onflow/cadence/bbq" + "github.com/onflow/cadence/bbq/opcode" + "github.com/onflow/cadence/common" + "github.com/onflow/cadence/sema" +) + +type ImportHandler func(location common.Location) *bbq.Program[opcode.Instruction] + +type LocationHandler func(identifiers []ast.Identifier, location common.Location) ([]ResolvedLocation, error) + +type ResolvedLocation = sema.ResolvedLocation + +func ResolveLocation( + locationHandler LocationHandler, + identifiers []ast.Identifier, + location common.Location, +) ([]ResolvedLocation, error) { + + // If no location handler is available, + // default to resolving to a single location that declares all identifiers + + if locationHandler == nil { + return []ResolvedLocation{ + { + Location: location, + Identifiers: identifiers, + }, + }, nil + } + + // A location handler is available, + // use it to resolve the location / identifiers + return locationHandler(identifiers, location) +} diff --git a/bbq/commons/utils.go b/bbq/commons/utils.go new file mode 100644 index 0000000000..006ac25baf --- /dev/null +++ b/bbq/commons/utils.go @@ -0,0 +1,51 @@ +/* + * 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 commons + +import ( + "bytes" + + "github.com/onflow/cadence/common" + "github.com/onflow/cadence/interpreter" +) + +func TypeQualifiedName(typeName, functionName string) string { + if typeName == "" { + return functionName + } + + return typeName + "." + functionName +} + +func LocationToBytes(location common.Location) ([]byte, error) { + var buf bytes.Buffer + enc := interpreter.CBOREncMode.NewStreamEncoder(&buf) + + err := interpreter.EncodeLocation(enc, location) + if err != nil { + return nil, err + } + + err = enc.Flush() + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} diff --git a/bbq/compiler/codegen.go b/bbq/compiler/codegen.go new file mode 100644 index 0000000000..24bf7c357a --- /dev/null +++ b/bbq/compiler/codegen.go @@ -0,0 +1,88 @@ +/* + * 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 compiler + +import ( + "github.com/onflow/cadence/bbq/opcode" + "github.com/onflow/cadence/errors" +) + +type CodeGen[E any] interface { + Offset() int + SetTarget(code *[]E) + Emit(instruction opcode.Instruction) + PatchJump(offset int, newTarget uint16) +} + +// ByteCodeGen is a CodeGen implementation that emits bytecode +type ByteCodeGen struct { + target *[]byte +} + +var _ CodeGen[byte] = &ByteCodeGen{} + +func (g *ByteCodeGen) Offset() int { + return len(*g.target) +} + +func (g *ByteCodeGen) SetTarget(target *[]byte) { + g.target = target +} + +func (g *ByteCodeGen) Emit(instruction opcode.Instruction) { + instruction.Encode(g.target) +} + +func (g *ByteCodeGen) PatchJump(offset int, newTarget uint16) { + opcode.PatchJump(g.target, offset, newTarget) +} + +// InstructionCodeGen is a CodeGen implementation that emits opcode.Instruction +type InstructionCodeGen struct { + target *[]opcode.Instruction +} + +var _ CodeGen[opcode.Instruction] = &InstructionCodeGen{} + +func (g *InstructionCodeGen) Offset() int { + return len(*g.target) +} + +func (g *InstructionCodeGen) SetTarget(target *[]opcode.Instruction) { + g.target = target +} + +func (g *InstructionCodeGen) Emit(instruction opcode.Instruction) { + *g.target = append(*g.target, instruction) +} + +func (g *InstructionCodeGen) PatchJump(offset int, newTarget uint16) { + switch ins := (*g.target)[offset].(type) { + case opcode.InstructionJump: + ins.Target = newTarget + (*g.target)[offset] = ins + + case opcode.InstructionJumpIfFalse: + ins.Target = newTarget + (*g.target)[offset] = ins + + default: + panic(errors.NewUnreachableError()) + } +} diff --git a/bbq/compiler/compiler.go b/bbq/compiler/compiler.go new file mode 100644 index 0000000000..57ad16baf0 --- /dev/null +++ b/bbq/compiler/compiler.go @@ -0,0 +1,1546 @@ +/* + * 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 compiler + +import ( + "math" + "strings" + + "github.com/onflow/cadence/ast" + "github.com/onflow/cadence/bbq" + "github.com/onflow/cadence/bbq/commons" + "github.com/onflow/cadence/bbq/constantkind" + "github.com/onflow/cadence/bbq/leb128" + "github.com/onflow/cadence/bbq/opcode" + "github.com/onflow/cadence/common" + "github.com/onflow/cadence/errors" + "github.com/onflow/cadence/interpreter" + "github.com/onflow/cadence/sema" +) + +type Compiler[E any] struct { + Program *ast.Program + Elaboration *sema.Elaboration + Config *Config[E] + + currentFunction *function[E] + compositeTypeStack *Stack[*sema.CompositeType] + + functions []*function[E] + constants []*constant + globals map[string]*global + importedGlobals map[string]*global + usedImportedGlobals []*global + loops []*loop + currentLoop *loop + staticTypes [][]byte + + // Cache alike for staticTypes and constants in the pool. + typesInPool map[sema.TypeID]uint16 + constantsInPool map[constantsCacheKey]*constant + + // TODO: initialize + memoryGauge common.MemoryGauge + + codeGen CodeGen[E] +} + +func (c *Compiler[_]) VisitAttachmentDeclaration(_ *ast.AttachmentDeclaration) (_ struct{}) { + //TODO implement me + panic("implement me") +} + +func (c *Compiler[_]) VisitEntitlementDeclaration(_ *ast.EntitlementDeclaration) (_ struct{}) { + //TODO implement me + panic("implement me") +} + +func (c *Compiler[_]) VisitEntitlementMappingDeclaration(_ *ast.EntitlementMappingDeclaration) (_ struct{}) { + //TODO implement me + panic("implement me") +} + +func (c *Compiler[_]) VisitRemoveStatement(_ *ast.RemoveStatement) (_ struct{}) { + //TODO implement me + panic("implement me") +} + +func (c *Compiler[_]) VisitAttachExpression(_ *ast.AttachExpression) (_ struct{}) { + //TODO implement me + panic("implement me") +} + +type constantsCacheKey struct { + data string + kind constantkind.ConstantKind +} + +var _ ast.DeclarationVisitor[struct{}] = &Compiler[any]{} +var _ ast.StatementVisitor[struct{}] = &Compiler[any]{} +var _ ast.ExpressionVisitor[struct{}] = &Compiler[any]{} + +func NewBytecodeCompiler( + program *ast.Program, + elaboration *sema.Elaboration, +) *Compiler[byte] { + return newCompiler( + program, + elaboration, + &ByteCodeGen{}, + ) +} + +func NewInstructionCompiler( + program *ast.Program, + elaboration *sema.Elaboration, +) *Compiler[opcode.Instruction] { + return newCompiler( + program, + elaboration, + &InstructionCodeGen{}, + ) +} + +func newCompiler[E any]( + program *ast.Program, + elaboration *sema.Elaboration, + codeGen CodeGen[E], +) *Compiler[E] { + return &Compiler[E]{ + Program: program, + Elaboration: elaboration, + Config: &Config[E]{}, + globals: make(map[string]*global), + importedGlobals: NativeFunctions(), + typesInPool: make(map[sema.TypeID]uint16), + constantsInPool: make(map[constantsCacheKey]*constant), + compositeTypeStack: &Stack[*sema.CompositeType]{ + elements: make([]*sema.CompositeType, 0), + }, + codeGen: codeGen, + } +} + +func (c *Compiler[_]) findGlobal(name string) *global { + global, ok := c.globals[name] + if ok { + return global + } + + // If failed to find, then try with type-qualified name. + // This is because contract functions/type-constructors can be accessed without the contract name. + // e.g: SomeContract.Foo() == Foo(), within `SomeContract`. + if !c.compositeTypeStack.isEmpty() { + enclosingContract := c.compositeTypeStack.bottom() + typeQualifiedName := commons.TypeQualifiedName(enclosingContract.Identifier, name) + global, ok = c.globals[typeQualifiedName] + if ok { + return global + } + } + + importedGlobal, ok := c.importedGlobals[name] + if !ok { + panic(errors.NewUnexpectedError("cannot find global declaration '%s'", name)) + } + + // Add the 'importedGlobal' to 'globals' when they are used for the first time. + // This way, the 'globals' would eventually have only the used imports. + // This is important since global indexes rely on this. + // + // If a global is found in imported globals, that means the index is not set. + // So set an index and add it to the 'globals'. + count := len(c.globals) + if count >= math.MaxUint16 { + panic(errors.NewUnexpectedError("invalid global declaration '%s'", name)) + } + importedGlobal.index = uint16(count) + c.globals[name] = importedGlobal + + // Also add it to the usedImportedGlobals. + // This is later used to export the imports, which is eventually used by the linker. + // Linker will link the imports in the same order as they are added here. + // i.e: same order as their indexes (preceded by globals defined in the current program). + // e.g: [global1, global2, ... [importedGlobal1, importedGlobal2, ...]]. + // Earlier we already reserved the indexes for the globals defined in the current program. + // (`reserveGlobalVars`) + + c.usedImportedGlobals = append(c.usedImportedGlobals, importedGlobal) + + return importedGlobal +} + +func (c *Compiler[_]) addGlobal(name string) *global { + count := len(c.globals) + if count >= math.MaxUint16 { + panic(errors.NewDefaultUserError("invalid global declaration")) + } + global := &global{ + index: uint16(count), + } + c.globals[name] = global + return global +} + +func (c *Compiler[_]) addImportedGlobal(location common.Location, name string) *global { + // Index is not set here. It is set only if this imported global is used. + global := &global{ + location: location, + name: name, + } + c.importedGlobals[name] = global + return global +} + +func (c *Compiler[E]) addFunction(name string, parameterCount uint16) *function[E] { + isCompositeFunction := !c.compositeTypeStack.isEmpty() + + function := newFunction[E](name, parameterCount, isCompositeFunction) + c.functions = append(c.functions, function) + c.currentFunction = function + c.codeGen.SetTarget(&function.code) + return function +} + +func (c *Compiler[_]) addConstant(kind constantkind.ConstantKind, data []byte) *constant { + count := len(c.constants) + if count >= math.MaxUint16 { + panic(errors.NewDefaultUserError("invalid constant declaration")) + } + + // Optimization: Reuse the constant if it is already added to the constant pool. + cacheKey := constantsCacheKey{ + data: string(data), + kind: kind, + } + if constant, ok := c.constantsInPool[cacheKey]; ok { + return constant + } + + constant := &constant{ + index: uint16(count), + kind: kind, + data: data[:], + } + c.constants = append(c.constants, constant) + c.constantsInPool[cacheKey] = constant + return constant +} + +func (c *Compiler[_]) stringConstLoad(str string) { + constant := c.addStringConst(str) + c.codeGen.Emit(opcode.InstructionGetConstant{ConstantIndex: constant.index}) +} + +func (c *Compiler[_]) addStringConst(str string) *constant { + return c.addConstant(constantkind.String, []byte(str)) +} + +func (c *Compiler[_]) emitJump(target int) int { + if target >= math.MaxUint16 { + panic(errors.NewDefaultUserError("invalid jump")) + } + offset := c.codeGen.Offset() + c.codeGen.Emit(opcode.InstructionJump{Target: uint16(target)}) + return offset +} + +func (c *Compiler[_]) emitUndefinedJump() int { + offset := c.codeGen.Offset() + c.codeGen.Emit(opcode.InstructionJump{Target: math.MaxUint16}) + return offset +} + +func (c *Compiler[_]) emitJumpIfFalse(target uint16) int { + if target >= math.MaxUint16 { + panic(errors.NewDefaultUserError("invalid jump")) + } + offset := c.codeGen.Offset() + c.codeGen.Emit(opcode.InstructionJumpIfFalse{Target: target}) + return offset +} + +func (c *Compiler[_]) emitUndefinedJumpIfFalse() int { + offset := c.codeGen.Offset() + c.codeGen.Emit(opcode.InstructionJumpIfFalse{Target: math.MaxUint16}) + return offset +} + +func (c *Compiler[_]) patchJump(opcodeOffset int) { + count := c.codeGen.Offset() + if count == 0 { + panic(errors.NewUnreachableError()) + } + if count >= math.MaxUint16 { + panic(errors.NewDefaultUserError("invalid jump")) + } + c.codeGen.PatchJump(opcodeOffset, uint16(count)) +} + +func (c *Compiler[_]) pushLoop(start int) { + loop := &loop{ + start: start, + } + c.loops = append(c.loops, loop) + c.currentLoop = loop +} + +func (c *Compiler[_]) popLoop() { + lastIndex := len(c.loops) - 1 + l := c.loops[lastIndex] + c.loops[lastIndex] = nil + c.loops = c.loops[:lastIndex] + + c.patchLoop(l) + + var previousLoop *loop + if lastIndex > 0 { + previousLoop = c.loops[lastIndex] + } + c.currentLoop = previousLoop +} + +func (c *Compiler[E]) Compile() *bbq.Program[E] { + + for _, declaration := range c.Program.ImportDeclarations() { + c.compileDeclaration(declaration) + } + + if c.Program.SoleContractInterfaceDeclaration() != nil { + return &bbq.Program[E]{ + Contract: c.exportContract(), + } + } + + compositeDeclarations := c.Program.CompositeDeclarations() + variableDeclarations := c.Program.VariableDeclarations() + functionDeclarations := c.Program.FunctionDeclarations() + + transaction := c.Program.SoleTransactionDeclaration() + if transaction != nil { + desugaredTransaction, desugaredTransactionParams, initFunction := c.desugarTransaction(transaction) + compositeDeclarations = append(compositeDeclarations, desugaredTransaction) + variableDeclarations = append(variableDeclarations, desugaredTransactionParams...) + if initFunction != nil { + functionDeclarations = append(functionDeclarations, initFunction) + } + } + + // Reserve globals for functions/types before visiting their implementations. + c.reserveGlobalVars( + "", + variableDeclarations, + nil, + functionDeclarations, + compositeDeclarations, + ) + + // Compile declarations + for _, declaration := range functionDeclarations { + c.compileDeclaration(declaration) + } + for _, declaration := range compositeDeclarations { + c.compileDeclaration(declaration) + } + + functions := c.ExportFunctions() + constants := c.exportConstants() + types := c.exportTypes() + imports := c.exportImports() + contract := c.exportContract() + variables := c.exportVariables(variableDeclarations) + + return &bbq.Program[E]{ + Functions: functions, + Constants: constants, + Types: types, + Imports: imports, + Contract: contract, + Variables: variables, + } +} + +func (c *Compiler[_]) reserveGlobalVars( + compositeTypeName string, + variableDecls []*ast.VariableDeclaration, + specialFunctionDecls []*ast.SpecialFunctionDeclaration, + functionDecls []*ast.FunctionDeclaration, + compositeDecls []*ast.CompositeDeclaration, +) { + for _, declaration := range variableDecls { + c.addGlobal(declaration.Identifier.Identifier) + } + + for _, declaration := range specialFunctionDecls { + switch declaration.Kind { + case common.DeclarationKindDestructorLegacy, + common.DeclarationKindPrepare: + // Important: All special functions visited within `VisitSpecialFunctionDeclaration` + // must be also visited here. And must be visited only them. e.g: Don't visit inits. + funcName := commons.TypeQualifiedName(compositeTypeName, declaration.FunctionDeclaration.Identifier.Identifier) + c.addGlobal(funcName) + } + } + + for _, declaration := range functionDecls { + funcName := commons.TypeQualifiedName(compositeTypeName, declaration.Identifier.Identifier) + c.addGlobal(funcName) + } + + for _, declaration := range compositeDecls { + // TODO: Handle nested composite types. Those name should be `Foo.Bar`. + qualifiedTypeName := commons.TypeQualifiedName(compositeTypeName, declaration.Identifier.Identifier) + + c.addGlobal(qualifiedTypeName) + + // For composite types other than contracts, global variables + // reserved by the type-name will be used for the init method. + // For contracts, global variables reserved by the type-name + // will be used for the contract value. + // Hence, reserve a separate global var for contract inits. + if declaration.CompositeKind == common.CompositeKindContract { + c.addGlobal(commons.InitFunctionName) + } + + // Define globals for functions before visiting function bodies. + c.reserveGlobalVars( + qualifiedTypeName, + nil, + declaration.Members.SpecialFunctions(), + declaration.Members.Functions(), + declaration.Members.Composites(), + ) + } +} + +func (c *Compiler[_]) exportConstants() []*bbq.Constant { + constants := make([]*bbq.Constant, 0, len(c.constants)) + for _, constant := range c.constants { + constants = append( + constants, + &bbq.Constant{ + Data: constant.data, + Kind: constant.kind, + }, + ) + } + return constants +} + +func (c *Compiler[_]) exportTypes() [][]byte { + return c.staticTypes +} + +func (c *Compiler[_]) exportImports() []*bbq.Import { + exportedImports := make([]*bbq.Import, 0) + for _, importedGlobal := range c.usedImportedGlobals { + bbqImport := &bbq.Import{ + Location: importedGlobal.location, + Name: importedGlobal.name, + } + exportedImports = append(exportedImports, bbqImport) + } + + return exportedImports +} + +func (c *Compiler[E]) ExportFunctions() []*bbq.Function[E] { + functions := make([]*bbq.Function[E], 0, len(c.functions)) + for _, function := range c.functions { + functions = append( + functions, + &bbq.Function[E]{ + Name: function.name, + Code: function.code, + LocalCount: function.localCount, + ParameterCount: function.parameterCount, + IsCompositeFunction: function.isCompositeFunction, + }, + ) + } + return functions +} + +func (c *Compiler[_]) exportVariables(variableDecls []*ast.VariableDeclaration) []*bbq.Variable { + variables := make([]*bbq.Variable, 0, len(c.functions)) + for _, varDecl := range variableDecls { + variables = append( + variables, + &bbq.Variable{ + Name: varDecl.Identifier.Identifier, + }, + ) + } + return variables +} + +func (c *Compiler[_]) exportContract() *bbq.Contract { + var location common.Location + var name string + + contractDecl := c.Program.SoleContractDeclaration() + if contractDecl != nil { + contractType := c.Elaboration.CompositeDeclarationType(contractDecl) + location = contractType.Location + name = contractType.Identifier + } else { + interfaceDecl := c.Program.SoleContractInterfaceDeclaration() + if interfaceDecl == nil { + return nil + } + + interfaceType := c.Elaboration.InterfaceDeclarationType(interfaceDecl) + location = interfaceType.Location + name = interfaceType.Identifier + } + + addressLocation := location.(common.AddressLocation) + return &bbq.Contract{ + Name: name, + Address: addressLocation.Address[:], + IsInterface: contractDecl == nil, + } +} + +func (c *Compiler[_]) compileDeclaration(declaration ast.Declaration) { + ast.AcceptDeclaration[struct{}](declaration, c) +} + +func (c *Compiler[_]) compileBlock(block *ast.Block) { + // TODO: scope + for _, statement := range block.Statements { + c.compileStatement(statement) + } +} + +func (c *Compiler[_]) compileFunctionBlock(functionBlock *ast.FunctionBlock) { + // TODO: pre and post conditions, incl. interfaces + if functionBlock == nil { + return + } + + c.compileBlock(functionBlock.Block) +} + +func (c *Compiler[_]) compileStatement(statement ast.Statement) { + ast.AcceptStatement[struct{}](statement, c) +} + +func (c *Compiler[_]) compileExpression(expression ast.Expression) { + ast.AcceptExpression[struct{}](expression, c) +} + +func (c *Compiler[_]) VisitReturnStatement(statement *ast.ReturnStatement) (_ struct{}) { + expression := statement.Expression + if expression != nil { + // TODO: copy + c.compileExpression(expression) + c.codeGen.Emit(opcode.InstructionReturnValue{}) + } else { + c.codeGen.Emit(opcode.InstructionReturn{}) + } + return +} + +func (c *Compiler[_]) VisitBreakStatement(_ *ast.BreakStatement) (_ struct{}) { + offset := c.codeGen.Offset() + c.currentLoop.breaks = append(c.currentLoop.breaks, offset) + c.emitUndefinedJump() + return +} + +func (c *Compiler[_]) VisitContinueStatement(_ *ast.ContinueStatement) (_ struct{}) { + c.emitJump(c.currentLoop.start) + return +} + +func (c *Compiler[_]) VisitIfStatement(statement *ast.IfStatement) (_ struct{}) { + // TODO: scope + switch test := statement.Test.(type) { + case ast.Expression: + c.compileExpression(test) + default: + // TODO: + panic(errors.NewUnreachableError()) + } + elseJump := c.emitUndefinedJumpIfFalse() + c.compileBlock(statement.Then) + elseBlock := statement.Else + if elseBlock != nil { + thenJump := c.emitUndefinedJump() + c.patchJump(elseJump) + c.compileBlock(elseBlock) + c.patchJump(thenJump) + } else { + c.patchJump(elseJump) + } + return +} + +func (c *Compiler[_]) VisitWhileStatement(statement *ast.WhileStatement) (_ struct{}) { + testOffset := c.codeGen.Offset() + c.pushLoop(testOffset) + c.compileExpression(statement.Test) + endJump := c.emitUndefinedJumpIfFalse() + c.compileBlock(statement.Block) + c.emitJump(testOffset) + c.patchJump(endJump) + c.popLoop() + return +} + +func (c *Compiler[_]) VisitForStatement(_ *ast.ForStatement) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +func (c *Compiler[_]) VisitEmitStatement(_ *ast.EmitStatement) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +func (c *Compiler[_]) VisitSwitchStatement(_ *ast.SwitchStatement) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +func (c *Compiler[_]) VisitVariableDeclaration(declaration *ast.VariableDeclaration) (_ struct{}) { + // TODO: second value + c.compileExpression(declaration.Value) + + varDeclTypes := c.Elaboration.VariableDeclarationTypes(declaration) + c.emitCheckType(varDeclTypes.TargetType) + + local := c.currentFunction.declareLocal(declaration.Identifier.Identifier) + c.codeGen.Emit(opcode.InstructionSetLocal{LocalIndex: local.index}) + return +} + +func (c *Compiler[_]) VisitAssignmentStatement(statement *ast.AssignmentStatement) (_ struct{}) { + c.compileExpression(statement.Value) + + assignmentTypes := c.Elaboration.AssignmentStatementTypes(statement) + c.emitCheckType(assignmentTypes.TargetType) + + switch target := statement.Target.(type) { + case *ast.IdentifierExpression: + varName := target.Identifier.Identifier + local := c.currentFunction.findLocal(varName) + if local != nil { + c.codeGen.Emit(opcode.InstructionSetLocal{LocalIndex: local.index}) + return + } + + global := c.findGlobal(varName) + c.codeGen.Emit(opcode.InstructionSetGlobal{GlobalIndex: global.index}) + + case *ast.MemberExpression: + c.compileExpression(target.Expression) + constant := c.addStringConst(target.Identifier.Identifier) + c.codeGen.Emit(opcode.InstructionSetField{FieldNameIndex: constant.index}) + + case *ast.IndexExpression: + c.compileExpression(target.TargetExpression) + c.compileExpression(target.IndexingExpression) + c.codeGen.Emit(opcode.InstructionSetIndex{}) + + default: + // TODO: + panic(errors.NewUnreachableError()) + } + return +} + +func (c *Compiler[_]) VisitSwapStatement(_ *ast.SwapStatement) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +func (c *Compiler[_]) VisitExpressionStatement(statement *ast.ExpressionStatement) (_ struct{}) { + c.compileExpression(statement.Expression) + + switch statement.Expression.(type) { + case *ast.DestroyExpression: + // Do nothing. Destroy operation will not produce any result. + default: + // Otherwise, drop the expression evaluation result. + c.codeGen.Emit(opcode.InstructionDrop{}) + } + + return +} + +func (c *Compiler[_]) VisitVoidExpression(_ *ast.VoidExpression) (_ struct{}) { + //TODO + panic(errors.NewUnreachableError()) +} + +func (c *Compiler[_]) VisitBoolExpression(expression *ast.BoolExpression) (_ struct{}) { + if expression.Value { + c.codeGen.Emit(opcode.InstructionTrue{}) + } else { + c.codeGen.Emit(opcode.InstructionFalse{}) + } + return +} + +func (c *Compiler[_]) VisitNilExpression(_ *ast.NilExpression) (_ struct{}) { + c.codeGen.Emit(opcode.InstructionNil{}) + return +} + +func (c *Compiler[_]) VisitIntegerExpression(expression *ast.IntegerExpression) (_ struct{}) { + integerType := c.Elaboration.IntegerExpressionType(expression) + constantKind := constantkind.FromSemaType(integerType) + + // TODO: + var data []byte + data = leb128.AppendInt64(data, expression.Value.Int64()) + + constant := c.addConstant(constantKind, data) + c.codeGen.Emit(opcode.InstructionGetConstant{ConstantIndex: constant.index}) + return +} + +func (c *Compiler[_]) VisitFixedPointExpression(_ *ast.FixedPointExpression) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +func (c *Compiler[_]) VisitArrayExpression(array *ast.ArrayExpression) (_ struct{}) { + arrayTypes := c.Elaboration.ArrayExpressionTypes(array) + + typeIndex := c.getOrAddType(arrayTypes.ArrayType) + + size := len(array.Values) + if size >= math.MaxUint16 { + panic(errors.NewDefaultUserError("invalid array expression")) + } + + for _, expression := range array.Values { + //EmitDup() + c.compileExpression(expression) + //EmitSetIndex(index) + } + + c.codeGen.Emit( + opcode.InstructionNewArray{ + TypeIndex: typeIndex, + Size: uint16(size), + IsResource: arrayTypes.ArrayType.IsResourceType(), + }, + ) + + return +} + +func (c *Compiler[_]) VisitDictionaryExpression(_ *ast.DictionaryExpression) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +func (c *Compiler[_]) VisitIdentifierExpression(expression *ast.IdentifierExpression) (_ struct{}) { + c.emitVariableLoad(expression.Identifier.Identifier) + return +} + +func (c *Compiler[_]) emitVariableLoad(name string) { + local := c.currentFunction.findLocal(name) + if local != nil { + c.codeGen.Emit(opcode.InstructionGetLocal{LocalIndex: local.index}) + return + } + + global := c.findGlobal(name) + c.codeGen.Emit(opcode.InstructionGetGlobal{GlobalIndex: global.index}) +} + +func (c *Compiler[_]) VisitInvocationExpression(expression *ast.InvocationExpression) (_ struct{}) { + // TODO: copy + + switch invokedExpr := expression.InvokedExpression.(type) { + case *ast.IdentifierExpression: + // TODO: Does constructors need any special handling? + //typ := c.Elaboration.IdentifierInInvocationType(invokedExpr) + //invocationType := typ.(*sema.FunctionType) + //if invocationType.IsConstructor { + //} + + // Load arguments + c.loadArguments(expression) + // Load function value + c.emitVariableLoad(invokedExpr.Identifier.Identifier) + + typeArgs := c.loadTypeArguments(expression) + c.codeGen.Emit(opcode.InstructionInvoke{TypeArgs: typeArgs}) + + case *ast.MemberExpression: + memberInfo, ok := c.Elaboration.MemberExpressionMemberAccessInfo(invokedExpr) + if !ok { + // TODO: verify + panic(errors.NewUnreachableError()) + } + + typeName := TypeName(memberInfo.AccessedType) + var funcName string + + invocationType := memberInfo.Member.TypeAnnotation.Type.(*sema.FunctionType) + if invocationType.IsConstructor { + funcName = commons.TypeQualifiedName(typeName, invokedExpr.Identifier.Identifier) + + // Calling a type constructor must be invoked statically. e.g: `SomeContract.Foo()`. + // Load arguments + c.loadArguments(expression) + // Load function value + c.emitVariableLoad(funcName) + + typeArgs := c.loadTypeArguments(expression) + c.codeGen.Emit(opcode.InstructionInvoke{TypeArgs: typeArgs}) + return + } + + // Receiver is loaded first. So 'self' is always the zero-th argument. + c.compileExpression(invokedExpr.Expression) + // Load arguments + c.loadArguments(expression) + + if isInterfaceMethodInvocation(memberInfo.AccessedType) { + funcName = invokedExpr.Identifier.Identifier + if len(funcName) >= math.MaxUint16 { + panic(errors.NewDefaultUserError("invalid function name")) + } + + typeArgs := c.loadTypeArguments(expression) + + argumentCount := len(expression.Arguments) + if argumentCount >= math.MaxUint16 { + panic(errors.NewDefaultUserError("invalid number of arguments")) + } + + funcNameConst := c.addStringConst(funcName) + c.codeGen.Emit( + opcode.InstructionInvokeDynamic{ + NameIndex: funcNameConst.index, + TypeArgs: typeArgs, + ArgCount: uint16(argumentCount), + }, + ) + + } else { + // Load function value + funcName = commons.TypeQualifiedName(typeName, invokedExpr.Identifier.Identifier) + c.emitVariableLoad(funcName) + + typeArgs := c.loadTypeArguments(expression) + c.codeGen.Emit(opcode.InstructionInvoke{TypeArgs: typeArgs}) + } + default: + panic(errors.NewUnreachableError()) + } + + return +} + +func isInterfaceMethodInvocation(accessedType sema.Type) bool { + switch typ := accessedType.(type) { + case *sema.ReferenceType: + return isInterfaceMethodInvocation(typ.Type) + case *sema.IntersectionType: + return true + default: + return false + } +} + +func TypeName(typ sema.Type) string { + switch typ := typ.(type) { + case *sema.ReferenceType: + return TypeName(typ.Type) + case *sema.IntersectionType: + // TODO: Revisit. Probably this is not needed here? + return TypeName(typ.Types[0]) + case *sema.CapabilityType: + return interpreter.PrimitiveStaticTypeCapability.String() + default: + return typ.QualifiedString() + } +} + +func (c *Compiler[_]) loadArguments(expression *ast.InvocationExpression) { + invocationTypes := c.Elaboration.InvocationExpressionTypes(expression) + for index, argument := range expression.Arguments { + c.compileExpression(argument.Expression) + c.emitCheckType(invocationTypes.ArgumentTypes[index]) + } + + // TODO: Is this needed? + //// Load empty values for optional parameters, if they are not provided. + //for i := len(expression.Arguments); i < invocationTypes.ParamCount; i++ { + // c.emit(opcode.Empty) + //} +} + +func (c *Compiler[_]) loadTypeArguments(expression *ast.InvocationExpression) []uint16 { + invocationTypes := c.Elaboration.InvocationExpressionTypes(expression) + + typeArgsCount := invocationTypes.TypeArguments.Len() + if typeArgsCount >= math.MaxUint16 { + panic(errors.NewDefaultUserError("invalid number of type arguments: %d", typeArgsCount)) + } + + if typeArgsCount == 0 { + return nil + } + + typeArgs := make([]uint16, 0, typeArgsCount) + + invocationTypes.TypeArguments.Foreach(func(key *sema.TypeParameter, typeParam sema.Type) { + typeArgs = append(typeArgs, c.getOrAddType(typeParam)) + }) + + return typeArgs +} + +func (c *Compiler[_]) VisitMemberExpression(expression *ast.MemberExpression) (_ struct{}) { + c.compileExpression(expression.Expression) + constant := c.addStringConst(expression.Identifier.Identifier) + c.codeGen.Emit(opcode.InstructionGetField{FieldNameIndex: constant.index}) + return +} + +func (c *Compiler[_]) VisitIndexExpression(expression *ast.IndexExpression) (_ struct{}) { + c.compileExpression(expression.TargetExpression) + c.compileExpression(expression.IndexingExpression) + c.codeGen.Emit(opcode.InstructionGetIndex{}) + return +} + +func (c *Compiler[_]) VisitConditionalExpression(_ *ast.ConditionalExpression) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +func (c *Compiler[_]) VisitUnaryExpression(expression *ast.UnaryExpression) (_ struct{}) { + switch expression.Operation { + case ast.OperationMove: + c.compileExpression(expression.Expression) + default: + // TODO + panic(errors.NewUnreachableError()) + } + + return +} + +func (c *Compiler[_]) VisitBinaryExpression(expression *ast.BinaryExpression) (_ struct{}) { + c.compileExpression(expression.Left) + // TODO: add support for other types + + switch expression.Operation { + case ast.OperationNilCoalesce: + // create a duplicate to perform the equal check. + // So if the condition succeeds, then the condition's result will be at the top of the stack. + c.codeGen.Emit(opcode.InstructionDup{}) + + c.codeGen.Emit(opcode.InstructionNil{}) + c.codeGen.Emit(opcode.InstructionEqual{}) + elseJump := c.emitUndefinedJumpIfFalse() + + // Drop the duplicated condition result. + // It is not needed for the 'then' path. + c.codeGen.Emit(opcode.InstructionDrop{}) + + c.compileExpression(expression.Right) + + thenJump := c.emitUndefinedJump() + c.patchJump(elseJump) + c.codeGen.Emit(opcode.InstructionUnwrap{}) + c.patchJump(thenJump) + default: + c.compileExpression(expression.Right) + + switch expression.Operation { + case ast.OperationPlus: + c.codeGen.Emit(opcode.InstructionIntAdd{}) + case ast.OperationMinus: + c.codeGen.Emit(opcode.InstructionIntSubtract{}) + case ast.OperationMul: + c.codeGen.Emit(opcode.InstructionIntMultiply{}) + case ast.OperationDiv: + c.codeGen.Emit(opcode.InstructionIntDivide{}) + case ast.OperationMod: + c.codeGen.Emit(opcode.InstructionIntMod{}) + case ast.OperationEqual: + c.codeGen.Emit(opcode.InstructionEqual{}) + case ast.OperationNotEqual: + c.codeGen.Emit(opcode.InstructionNotEqual{}) + case ast.OperationLess: + c.codeGen.Emit(opcode.InstructionIntLess{}) + case ast.OperationLessEqual: + c.codeGen.Emit(opcode.InstructionIntLessOrEqual{}) + case ast.OperationGreater: + c.codeGen.Emit(opcode.InstructionIntGreater{}) + case ast.OperationGreaterEqual: + c.codeGen.Emit(opcode.InstructionIntGreaterOrEqual{}) + default: + panic(errors.NewUnreachableError()) + } + } + + return +} + +func (c *Compiler[_]) VisitFunctionExpression(_ *ast.FunctionExpression) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +func (c *Compiler[_]) VisitStringExpression(expression *ast.StringExpression) (_ struct{}) { + c.stringConstLoad(expression.Value) + return +} + +func (c *Compiler[_]) VisitStringTemplateExpression(_ *ast.StringTemplateExpression) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +func (c *Compiler[_]) VisitCastingExpression(expression *ast.CastingExpression) (_ struct{}) { + c.compileExpression(expression.Expression) + + castingTypes := c.Elaboration.CastingExpressionTypes(expression) + index := c.getOrAddType(castingTypes.TargetType) + + castKind := opcode.CastKindFrom(expression.Operation) + + c.codeGen.Emit( + opcode.InstructionCast{ + TypeIndex: index, + Kind: castKind, + }, + ) + return +} + +func (c *Compiler[_]) VisitCreateExpression(expression *ast.CreateExpression) (_ struct{}) { + c.compileExpression(expression.InvocationExpression) + return +} + +func (c *Compiler[_]) VisitDestroyExpression(expression *ast.DestroyExpression) (_ struct{}) { + c.compileExpression(expression.Expression) + c.codeGen.Emit(opcode.InstructionDestroy{}) + return +} + +func (c *Compiler[_]) VisitReferenceExpression(expression *ast.ReferenceExpression) (_ struct{}) { + c.compileExpression(expression.Expression) + borrowType := c.Elaboration.ReferenceExpressionBorrowType(expression) + index := c.getOrAddType(borrowType) + c.codeGen.Emit(opcode.InstructionNewRef{TypeIndex: index}) + return +} + +func (c *Compiler[_]) VisitForceExpression(_ *ast.ForceExpression) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +func (c *Compiler[_]) VisitPathExpression(expression *ast.PathExpression) (_ struct{}) { + domain := common.PathDomainFromIdentifier(expression.Domain.Identifier) + identifier := expression.Identifier.Identifier + if len(identifier) >= math.MaxUint16 { + panic(errors.NewDefaultUserError("invalid identifier")) + } + + identifierConst := c.addStringConst(identifier) + + c.codeGen.Emit( + opcode.InstructionPath{ + Domain: domain, + IdentifierIndex: identifierConst.index, + }, + ) + return +} + +func (c *Compiler[_]) VisitSpecialFunctionDeclaration(declaration *ast.SpecialFunctionDeclaration) (_ struct{}) { + kind := declaration.DeclarationKind() + switch kind { + case common.DeclarationKindInitializer: + c.compileInitializer(declaration) + case common.DeclarationKindDestructorLegacy, common.DeclarationKindPrepare: + c.compileDeclaration(declaration.FunctionDeclaration) + default: + // TODO: support other special functions + panic(errors.NewUnreachableError()) + } + return +} + +func (c *Compiler[_]) compileInitializer(declaration *ast.SpecialFunctionDeclaration) { + enclosingCompositeTypeName := c.enclosingCompositeTypeFullyQualifiedName() + enclosingType := c.compositeTypeStack.top() + + var functionName string + if enclosingType.Kind == common.CompositeKindContract { + // For contracts, add the initializer as `init()`. + // A global variable with the same name as contract is separately added. + // The VM will load the contract and assign to that global variable during imports resolution. + functionName = declaration.DeclarationIdentifier().Identifier + } else { + // Use the type name as the function name for initializer. + // So `x = Foo()` would directly call the init method. + functionName = enclosingCompositeTypeName + } + + parameterCount := 0 + parameterList := declaration.FunctionDeclaration.ParameterList + if parameterList != nil { + parameterCount = len(parameterList.Parameters) + } + + if parameterCount > math.MaxUint16 { + panic(errors.NewDefaultUserError("invalid parameter count")) + } + + function := c.addFunction(functionName, uint16(parameterCount)) + c.declareParameters(function, parameterList, false) + + // Declare `self` + self := c.currentFunction.declareLocal(sema.SelfIdentifier) + + // Initialize an empty struct and assign to `self`. + // i.e: `self = New()` + + enclosingCompositeType := c.compositeTypeStack.top() + + // Write composite kind + // TODO: Maybe get/include this from static-type. Then no need to provide separately. + kind := enclosingCompositeType.Kind + + typeIndex := c.getOrAddType(enclosingCompositeType) + + c.codeGen.Emit( + opcode.InstructionNew{ + Kind: kind, + TypeIndex: typeIndex, + }, + ) + + if enclosingType.Kind == common.CompositeKindContract { + // During contract init, update the global variable with the newly initialized contract value. + // So accessing the contract through the global variable while initializing itself, would work. + // i.e: + // contract Foo { + // init() { + // Foo.something() // <-- accessing `Foo` while initializing `Foo` + // } + // } + + // Duplicate the top of stack and store it in both global variable and in `self` + c.codeGen.Emit(opcode.InstructionDup{}) + global := c.findGlobal(enclosingCompositeTypeName) + + c.codeGen.Emit(opcode.InstructionSetGlobal{GlobalIndex: global.index}) + } + + c.codeGen.Emit(opcode.InstructionSetLocal{LocalIndex: self.index}) + + // emit for the statements in `init()` body. + c.compileFunctionBlock(declaration.FunctionDeclaration.FunctionBlock) + + // Constructor should return the created the struct. i.e: return `self` + c.codeGen.Emit(opcode.InstructionGetLocal{LocalIndex: self.index}) + c.codeGen.Emit(opcode.InstructionReturnValue{}) +} + +func (c *Compiler[_]) VisitFunctionDeclaration(declaration *ast.FunctionDeclaration) (_ struct{}) { + // TODO: handle nested functions + declareReceiver := !c.compositeTypeStack.isEmpty() + function := c.declareFunction(declaration, declareReceiver) + + c.declareParameters(function, declaration.ParameterList, declareReceiver) + c.compileFunctionBlock(declaration.FunctionBlock) + + // Manually emit a return, if there are no explicit return statements. + if !declaration.FunctionBlock.HasStatements() { + c.codeGen.Emit(opcode.InstructionReturn{}) + } else { + statements := declaration.FunctionBlock.Block.Statements + lastStmt := statements[len(statements)-1] + if _, isReturn := lastStmt.(*ast.ReturnStatement); !isReturn { + c.codeGen.Emit(opcode.InstructionReturn{}) + } + } + + return +} + +func (c *Compiler[E]) declareFunction(declaration *ast.FunctionDeclaration, declareReceiver bool) *function[E] { + enclosingCompositeTypeName := c.enclosingCompositeTypeFullyQualifiedName() + functionName := commons.TypeQualifiedName(enclosingCompositeTypeName, declaration.Identifier.Identifier) + + parameterCount := 0 + + paramList := declaration.ParameterList + if paramList != nil { + parameterCount = len(paramList.Parameters) + } + + if parameterCount >= math.MaxUint16 { + panic(errors.NewDefaultUserError("invalid parameter count")) + } + + if declareReceiver { + parameterCount++ + } + + return c.addFunction(functionName, uint16(parameterCount)) +} + +func (c *Compiler[_]) VisitCompositeDeclaration(declaration *ast.CompositeDeclaration) (_ struct{}) { + enclosingCompositeType := c.Elaboration.CompositeDeclarationType(declaration) + c.compositeTypeStack.push(enclosingCompositeType) + defer func() { + c.compositeTypeStack.pop() + }() + + // Compile members + hasInit := false + for _, specialFunc := range declaration.Members.SpecialFunctions() { + if specialFunc.Kind == common.DeclarationKindInitializer { + hasInit = true + } + c.compileDeclaration(specialFunc) + } + + // If the initializer is not declared, generate an empty initializer. + if !hasInit { + c.generateEmptyInit() + } + + for _, function := range declaration.Members.Functions() { + c.compileDeclaration(function) + } + + for _, nestedTypes := range declaration.Members.Composites() { + c.compileDeclaration(nestedTypes) + } + + // TODO: + + return +} + +func (c *Compiler[_]) VisitInterfaceDeclaration(_ *ast.InterfaceDeclaration) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +func (c *Compiler[_]) VisitFieldDeclaration(_ *ast.FieldDeclaration) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +func (c *Compiler[_]) VisitPragmaDeclaration(_ *ast.PragmaDeclaration) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +func (c *Compiler[_]) VisitImportDeclaration(declaration *ast.ImportDeclaration) (_ struct{}) { + resolvedLocation, err := commons.ResolveLocation( + c.Config.LocationHandler, + declaration.Identifiers, + declaration.Location, + ) + if err != nil { + panic(err) + } + + for _, location := range resolvedLocation { + importedProgram := c.Config.ImportHandler(location.Location) + + // Add a global variable for the imported contract value. + contractDecl := importedProgram.Contract + isContract := contractDecl != nil + if isContract { + c.addImportedGlobal(location.Location, contractDecl.Name) + } + + for _, function := range importedProgram.Functions { + name := function.Name + + // Skip the contract initializer. + // It should never be able to invoked within the code. + if isContract && name == commons.InitFunctionName { + continue + } + + // TODO: Filter-in only public functions + c.addImportedGlobal(location.Location, function.Name) + } + } + + return +} + +func (c *Compiler[_]) VisitTransactionDeclaration(_ *ast.TransactionDeclaration) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +func (c *Compiler[_]) VisitEnumCaseDeclaration(_ *ast.EnumCaseDeclaration) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +func (c *Compiler[_]) patchLoop(l *loop) { + for _, breakOffset := range l.breaks { + c.patchJump(breakOffset) + } +} + +func (c *Compiler[_]) emitCheckType(targetType sema.Type) { + index := c.getOrAddType(targetType) + + c.codeGen.Emit(opcode.InstructionTransfer{TypeIndex: index}) +} + +func (c *Compiler[_]) getOrAddType(targetType sema.Type) uint16 { + // Optimization: Re-use types in the pool. + index, ok := c.typesInPool[targetType.ID()] + if !ok { + staticType := interpreter.ConvertSemaToStaticType(c.memoryGauge, targetType) + bytes, err := interpreter.StaticTypeToBytes(staticType) + if err != nil { + panic(err) + } + index = c.addType(bytes) + c.typesInPool[targetType.ID()] = index + } + return index +} + +func (c *Compiler[_]) addType(data []byte) uint16 { + count := len(c.staticTypes) + if count >= math.MaxUint16 { + panic(errors.NewDefaultUserError("invalid type declaration")) + } + + c.staticTypes = append(c.staticTypes, data) + return uint16(count) +} + +func (c *Compiler[_]) enclosingCompositeTypeFullyQualifiedName() string { + if c.compositeTypeStack.isEmpty() { + return "" + } + + var sb strings.Builder + for i, typ := range c.compositeTypeStack.elements { + if i > 0 { + sb.WriteRune('.') + } + sb.WriteString(typ.Identifier) + } + + return sb.String() +} + +func (c *Compiler[E]) declareParameters(function *function[E], paramList *ast.ParameterList, declareReceiver bool) { + if declareReceiver { + // Declare receiver as `self`. + // Receiver is always at the zero-th index of params. + function.declareLocal(sema.SelfIdentifier) + } + + if paramList != nil { + for _, parameter := range paramList.Parameters { + parameterName := parameter.Identifier.Identifier + function.declareLocal(parameterName) + } + } +} + +// desugarTransaction Convert a transaction into a composite type declaration, +// so the code-gen would seamlessly work without having special-case anything in compiler/vm. +// Transaction parameters are converted into global variables. +// An initializer is generated to set parameters to above generated global variables. +func (c *Compiler[_]) desugarTransaction(transaction *ast.TransactionDeclaration) ( + *ast.CompositeDeclaration, + []*ast.VariableDeclaration, + *ast.FunctionDeclaration, +) { + // TODO: This assumes the transaction program/elaboration is not cached. + // i.e: Modifies the elaboration. + // Handle this properly for cached transactions. + + // TODO: add pre/post conditions + + var varDeclarations []*ast.VariableDeclaration + var initFunction *ast.FunctionDeclaration + + if transaction.ParameterList != nil { + varDeclarations = make([]*ast.VariableDeclaration, 0, len(transaction.ParameterList.Parameters)) + statements := make([]ast.Statement, 0, len(transaction.ParameterList.Parameters)) + parameters := make([]*ast.Parameter, 0, len(transaction.ParameterList.Parameters)) + + for index, parameter := range transaction.ParameterList.Parameters { + // Create global variables + // i.e: `var a: Type` + field := &ast.VariableDeclaration{ + Access: ast.AccessSelf, + IsConstant: false, + Identifier: parameter.Identifier, + TypeAnnotation: parameter.TypeAnnotation, + } + varDeclarations = append(varDeclarations, field) + + // Create assignment from param to global var. + // i.e: `a = $param_a` + modifiedParamName := commons.TransactionGeneratedParamPrefix + parameter.Identifier.Identifier + modifiedParameter := &ast.Parameter{ + Label: "", + Identifier: ast.Identifier{ + Identifier: modifiedParamName, + }, + TypeAnnotation: parameter.TypeAnnotation, + } + parameters = append(parameters, modifiedParameter) + + assignment := &ast.AssignmentStatement{ + Target: &ast.IdentifierExpression{ + Identifier: parameter.Identifier, + }, + Value: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: modifiedParamName, + }, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + }, + } + statements = append(statements, assignment) + + transactionTypes := c.Elaboration.TransactionDeclarationType(transaction) + paramType := transactionTypes.Parameters[index].TypeAnnotation.Type + assignmentTypes := sema.AssignmentStatementTypes{ + ValueType: paramType, + TargetType: paramType, + } + + c.Elaboration.SetAssignmentStatementTypes(assignment, assignmentTypes) + } + + // Create an init function. + // func $init($param_a: Type, $param_b: Type, ...) { + // a = $param_a + // b = $param_b + // ... + // } + initFunction = &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: commons.ProgramInitFunctionName, + }, + ParameterList: &ast.ParameterList{ + Parameters: parameters, + }, + ReturnTypeAnnotation: nil, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Statements: statements, + }, + }, + } + } + + var members []ast.Declaration + if transaction.Execute != nil { + members = append(members, transaction.Execute.FunctionDeclaration) + } + if transaction.Prepare != nil { + members = append(members, transaction.Prepare) + } + + compositeType := &sema.CompositeType{ + Location: nil, + Identifier: commons.TransactionWrapperCompositeName, + Kind: common.CompositeKindStructure, + } + + compositeDecl := ast.NewCompositeDeclaration( + c.memoryGauge, + ast.AccessNotSpecified, + common.CompositeKindStructure, + ast.NewIdentifier( + c.memoryGauge, + commons.TransactionWrapperCompositeName, + ast.EmptyPosition, + ), + nil, + ast.NewMembers(c.memoryGauge, members), + "", + ast.EmptyRange, + ) + + c.Elaboration.SetCompositeDeclarationType(compositeDecl, compositeType) + + return compositeDecl, varDeclarations, initFunction +} + +var emptyInitializer = func() *ast.SpecialFunctionDeclaration { + // This is created only once per compilation. So no need to meter memory. + + initializer := ast.NewFunctionDeclaration( + nil, + ast.AccessNotSpecified, + ast.FunctionPurityUnspecified, + false, + false, + ast.NewIdentifier( + nil, + commons.InitFunctionName, + ast.EmptyPosition, + ), + nil, + nil, + nil, + ast.NewFunctionBlock( + nil, + ast.NewBlock(nil, nil, ast.EmptyRange), + nil, + nil, + ), + ast.Position{}, + "", + ) + + return ast.NewSpecialFunctionDeclaration( + nil, + common.DeclarationKindInitializer, + initializer, + ) +}() + +func (c *Compiler[_]) generateEmptyInit() { + c.VisitSpecialFunctionDeclaration(emptyInitializer) +} diff --git a/bbq/compiler/compiler_test.go b/bbq/compiler/compiler_test.go new file mode 100644 index 0000000000..acdf4211d6 --- /dev/null +++ b/bbq/compiler/compiler_test.go @@ -0,0 +1,333 @@ +/* + * 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 compiler + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/onflow/cadence/bbq" + "github.com/onflow/cadence/bbq/constantkind" + "github.com/onflow/cadence/bbq/opcode" + . "github.com/onflow/cadence/test_utils/sema_utils" +) + +func TestCompileRecursionFib(t *testing.T) { + + t.Parallel() + + checker, err := ParseAndCheck(t, ` + fun fib(_ n: Int): Int { + if n < 2 { + return n + } + return fib(n - 1) + fib(n - 2) + } + `) + require.NoError(t, err) + + compiler := NewBytecodeCompiler(checker.Program, checker.Elaboration) + program := compiler.Compile() + + require.Len(t, program.Functions, 1) + require.Equal(t, + []byte{ + // if n < 2 + byte(opcode.GetLocal), 0, 0, + byte(opcode.GetConstant), 0, 0, + byte(opcode.IntLess), + byte(opcode.JumpIfFalse), 0, 14, + // then return n + byte(opcode.GetLocal), 0, 0, + byte(opcode.ReturnValue), + // fib(n - 1) + byte(opcode.GetLocal), 0, 0, + byte(opcode.GetConstant), 0, 1, + byte(opcode.IntSubtract), + byte(opcode.Transfer), 0, 0, + byte(opcode.GetGlobal), 0, 0, + byte(opcode.Invoke), 0, 0, + // fib(n - 2) + byte(opcode.GetLocal), 0, 0, + byte(opcode.GetConstant), 0, 0, + byte(opcode.IntSubtract), + byte(opcode.Transfer), 0, 0, + byte(opcode.GetGlobal), 0, 0, + byte(opcode.Invoke), 0, 0, + // return sum + byte(opcode.IntAdd), + byte(opcode.ReturnValue), + }, + compiler.ExportFunctions()[0].Code, + ) + + require.Equal(t, + []*bbq.Constant{ + { + Data: []byte{0x2}, + Kind: constantkind.Int, + }, + { + Data: []byte{0x1}, + Kind: constantkind.Int, + }, + }, + program.Constants, + ) +} + +func TestCompileImperativeFib(t *testing.T) { + + t.Parallel() + + checker, err := ParseAndCheck(t, ` + fun fib(_ n: Int): Int { + var fib1 = 1 + var fib2 = 1 + var fibonacci = fib1 + var i = 2 + while i < n { + fibonacci = fib1 + fib2 + fib1 = fib2 + fib2 = fibonacci + i = i + 1 + } + return fibonacci + } + `) + require.NoError(t, err) + + compiler := NewBytecodeCompiler(checker.Program, checker.Elaboration) + program := compiler.Compile() + + require.Len(t, program.Functions, 1) + require.Equal(t, + []byte{ + // var fib1 = 1 + byte(opcode.GetConstant), 0, 0, + byte(opcode.Transfer), 0, 0, + byte(opcode.SetLocal), 0, 1, + // var fib2 = 1 + byte(opcode.GetConstant), 0, 0, + byte(opcode.Transfer), 0, 0, + byte(opcode.SetLocal), 0, 2, + // var fibonacci = fib1 + byte(opcode.GetLocal), 0, 1, + byte(opcode.Transfer), 0, 0, + byte(opcode.SetLocal), 0, 3, + // var i = 2 + byte(opcode.GetConstant), 0, 1, + byte(opcode.Transfer), 0, 0, + byte(opcode.SetLocal), 0, 4, + // while i < n + byte(opcode.GetLocal), 0, 4, + byte(opcode.GetLocal), 0, 0, + byte(opcode.IntLess), + byte(opcode.JumpIfFalse), 0, 93, + // fibonacci = fib1 + fib2 + byte(opcode.GetLocal), 0, 1, + byte(opcode.GetLocal), 0, 2, + byte(opcode.IntAdd), + byte(opcode.Transfer), 0, 0, + byte(opcode.SetLocal), 0, 3, + // fib1 = fib2 + byte(opcode.GetLocal), 0, 2, + byte(opcode.Transfer), 0, 0, + byte(opcode.SetLocal), 0, 1, + // fib2 = fibonacci + byte(opcode.GetLocal), 0, 3, + byte(opcode.Transfer), 0, 0, + byte(opcode.SetLocal), 0, 2, + // i = i + 1 + byte(opcode.GetLocal), 0, 4, + byte(opcode.GetConstant), 0, 0, + byte(opcode.IntAdd), + byte(opcode.Transfer), 0, 0, + byte(opcode.SetLocal), 0, 4, + // continue loop + byte(opcode.Jump), 0, 36, + // return fibonacci + byte(opcode.GetLocal), 0, 3, + byte(opcode.ReturnValue), + }, + compiler.ExportFunctions()[0].Code, + ) + + require.Equal(t, + []*bbq.Constant{ + { + Data: []byte{0x1}, + Kind: constantkind.Int, + }, + { + Data: []byte{0x2}, + Kind: constantkind.Int, + }, + }, + program.Constants, + ) +} + +func TestCompileBreak(t *testing.T) { + + t.Parallel() + + checker, err := ParseAndCheck(t, ` + fun test(): Int { + var i = 0 + while true { + if i > 3 { + break + } + i = i + 1 + } + return i + } + `) + require.NoError(t, err) + + compiler := NewBytecodeCompiler(checker.Program, checker.Elaboration) + program := compiler.Compile() + + require.Len(t, program.Functions, 1) + require.Equal(t, + []byte{ + // var i = 0 + byte(opcode.GetConstant), 0, 0, + byte(opcode.Transfer), 0, 0, + byte(opcode.SetLocal), 0, 0, + // while true + byte(opcode.True), + byte(opcode.JumpIfFalse), 0, 42, + // if i > 3 + byte(opcode.GetLocal), 0, 0, + byte(opcode.GetConstant), 0, 1, + byte(opcode.IntGreater), + byte(opcode.JumpIfFalse), 0, 26, + // break + byte(opcode.Jump), 0, 42, + // i = i + 1 + byte(opcode.GetLocal), 0, 0, + byte(opcode.GetConstant), 0, 2, + byte(opcode.IntAdd), + byte(opcode.Transfer), 0, 0, + byte(opcode.SetLocal), 0, 0, + // repeat + byte(opcode.Jump), 0, 9, + // return i + byte(opcode.GetLocal), 0, 0, + byte(opcode.ReturnValue), + }, + compiler.ExportFunctions()[0].Code, + ) + + require.Equal(t, + []*bbq.Constant{ + { + Data: []byte{0x0}, + Kind: constantkind.Int, + }, + { + Data: []byte{0x3}, + Kind: constantkind.Int, + }, + { + Data: []byte{0x1}, + Kind: constantkind.Int, + }, + }, + program.Constants, + ) +} + +func TestCompileContinue(t *testing.T) { + + t.Parallel() + + checker, err := ParseAndCheck(t, ` + fun test(): Int { + var i = 0 + while true { + i = i + 1 + if i < 3 { + continue + } + break + } + return i + } + `) + require.NoError(t, err) + + compiler := NewBytecodeCompiler(checker.Program, checker.Elaboration) + program := compiler.Compile() + + require.Len(t, program.Functions, 1) + require.Equal(t, + []byte{ + // var i = 0 + byte(opcode.GetConstant), 0, 0, + byte(opcode.Transfer), 0, 0, + byte(opcode.SetLocal), 0, 0, + // while true + byte(opcode.True), + byte(opcode.JumpIfFalse), 0, 45, + // i = i + 1 + byte(opcode.GetLocal), 0, 0, + byte(opcode.GetConstant), 0, 1, + byte(opcode.IntAdd), + byte(opcode.Transfer), 0, 0, + byte(opcode.SetLocal), 0, 0, + // if i < 3 + byte(opcode.GetLocal), 0, 0, + byte(opcode.GetConstant), 0, 2, + byte(opcode.IntLess), + byte(opcode.JumpIfFalse), 0, 39, + // continue + byte(opcode.Jump), 0, 9, + // break + byte(opcode.Jump), 0, 45, + // repeat + byte(opcode.Jump), 0, 9, + // return i + byte(opcode.GetLocal), 0, 0, + byte(opcode.ReturnValue), + }, + compiler.ExportFunctions()[0].Code, + ) + + require.Equal(t, + []*bbq.Constant{ + { + Data: []byte{0x0}, + Kind: constantkind.Int, + }, + { + Data: []byte{0x1}, + Kind: constantkind.Int, + }, + { + Data: []byte{0x3}, + Kind: constantkind.Int, + }, + }, + program.Constants, + ) +} diff --git a/bbq/compiler/config.go b/bbq/compiler/config.go new file mode 100644 index 0000000000..3f55fff4c9 --- /dev/null +++ b/bbq/compiler/config.go @@ -0,0 +1,28 @@ +/* + * 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 compiler + +import ( + "github.com/onflow/cadence/bbq/commons" +) + +type Config[E any] struct { + ImportHandler commons.ImportHandler + LocationHandler commons.LocationHandler +} diff --git a/bbq/compiler/constant.go b/bbq/compiler/constant.go new file mode 100644 index 0000000000..643eab08d3 --- /dev/null +++ b/bbq/compiler/constant.go @@ -0,0 +1,27 @@ +/* + * 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 compiler + +import "github.com/onflow/cadence/bbq/constantkind" + +type constant struct { + index uint16 + data []byte + kind constantkind.ConstantKind +} diff --git a/bbq/compiler/function.go b/bbq/compiler/function.go new file mode 100644 index 0000000000..132b3f20af --- /dev/null +++ b/bbq/compiler/function.go @@ -0,0 +1,59 @@ +/* + * 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 compiler + +import ( + "math" + + "github.com/onflow/cadence/activations" + "github.com/onflow/cadence/errors" +) + +type function[E any] struct { + name string + code []E + localCount uint16 + locals *activations.Activations[*local] + parameterCount uint16 + isCompositeFunction bool +} + +func newFunction[E any](name string, parameterCount uint16, isCompositeFunction bool) *function[E] { + return &function[E]{ + name: name, + parameterCount: parameterCount, + locals: activations.NewActivations[*local](nil), + isCompositeFunction: isCompositeFunction, + } +} + +func (f *function[E]) declareLocal(name string) *local { + if f.localCount >= math.MaxUint16 { + panic(errors.NewDefaultUserError("invalid local declaration")) + } + index := f.localCount + f.localCount++ + local := &local{index: index} + f.locals.Set(name, local) + return local +} + +func (f *function[E]) findLocal(name string) *local { + return f.locals.Find(name) +} diff --git a/bbq/compiler/global.go b/bbq/compiler/global.go new file mode 100644 index 0000000000..8ace0c0475 --- /dev/null +++ b/bbq/compiler/global.go @@ -0,0 +1,27 @@ +/* + * 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 compiler + +import "github.com/onflow/cadence/common" + +type global struct { + name string + location common.Location + index uint16 +} diff --git a/bbq/compiler/local.go b/bbq/compiler/local.go new file mode 100644 index 0000000000..53620359bc --- /dev/null +++ b/bbq/compiler/local.go @@ -0,0 +1,23 @@ +/* + * 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 compiler + +type local struct { + index uint16 +} diff --git a/bbq/compiler/loop.go b/bbq/compiler/loop.go new file mode 100644 index 0000000000..f91c5ebc96 --- /dev/null +++ b/bbq/compiler/loop.go @@ -0,0 +1,24 @@ +/* + * 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 compiler + +type loop struct { + breaks []int + start int +} diff --git a/bbq/compiler/native_functions.go b/bbq/compiler/native_functions.go new file mode 100644 index 0000000000..80e31d7ea1 --- /dev/null +++ b/bbq/compiler/native_functions.go @@ -0,0 +1,82 @@ +/* + * 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 compiler + +import ( + "github.com/onflow/cadence/sema" + + "github.com/onflow/cadence/bbq/commons" +) + +var nativeFunctions []*global + +func NativeFunctions() map[string]*global { + funcs := make(map[string]*global, len(nativeFunctions)) + for _, value := range nativeFunctions { + funcs[value.name] = value + } + return funcs +} + +var builtinTypes = []sema.Type{ + sema.StringType, + sema.AccountType, + &sema.CapabilityType{}, +} + +var stdlibFunctions = []string{ + commons.LogFunctionName, + commons.PanicFunctionName, + commons.GetAccountFunctionName, +} + +func init() { + // Here the order isn't really important. + // Because the native functions used by a program are also + // added to the imports section of the compiled program. + // Then the VM will link the imports (native functions) by the name. + for _, typ := range builtinTypes { + registerBoundFunctions(typ) + } + + for _, funcName := range stdlibFunctions { + addNativeFunction(funcName) + } +} + +func registerBoundFunctions(typ sema.Type) { + for name, _ := range typ.GetMembers() { + funcName := commons.TypeQualifiedName(typ.QualifiedString(), name) + addNativeFunction(funcName) + } + + compositeType, ok := typ.(*sema.CompositeType) + if ok && compositeType.NestedTypes != nil { + compositeType.NestedTypes.Foreach(func(_ string, nestedType sema.Type) { + registerBoundFunctions(nestedType) + }) + } +} + +func addNativeFunction(name string) { + global := &global{ + name: name, + } + nativeFunctions = append(nativeFunctions, global) +} diff --git a/bbq/compiler/stack.go b/bbq/compiler/stack.go new file mode 100644 index 0000000000..2251070ed6 --- /dev/null +++ b/bbq/compiler/stack.go @@ -0,0 +1,47 @@ +/* + * 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 compiler + +type Stack[T any] struct { + elements []T +} + +func (s *Stack[T]) push(typ T) { + s.elements = append(s.elements, typ) +} + +func (s *Stack[T]) pop() T { + lastIndex := len(s.elements) - 1 + top := s.elements[lastIndex] + s.elements = s.elements[:lastIndex] + return top +} + +func (s *Stack[T]) top() T { + lastIndex := len(s.elements) - 1 + return s.elements[lastIndex] +} + +func (s *Stack[T]) bottom() T { + return s.elements[0] +} + +func (s *Stack[T]) isEmpty() bool { + return len(s.elements) == 0 +} diff --git a/bbq/constant.go b/bbq/constant.go new file mode 100644 index 0000000000..57889cedb6 --- /dev/null +++ b/bbq/constant.go @@ -0,0 +1,26 @@ +/* + * 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 bbq + +import "github.com/onflow/cadence/bbq/constantkind" + +type Constant struct { + Data []byte + Kind constantkind.ConstantKind +} diff --git a/bbq/constantkind/constantkind.go b/bbq/constantkind/constantkind.go new file mode 100644 index 0000000000..5d8caa584c --- /dev/null +++ b/bbq/constantkind/constantkind.go @@ -0,0 +1,137 @@ +/* + * 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 constantkind + +import ( + "github.com/onflow/cadence/errors" + "github.com/onflow/cadence/sema" +) + +//go:generate go run golang.org/x/tools/cmd/stringer -type=ConstantKind + +type ConstantKind uint8 + +const ( + Unknown ConstantKind = iota + String + + // Int* + Int + Int8 + Int16 + Int32 + Int64 + Int128 + Int256 + _ + + // UInt* + UInt + UInt8 + UInt16 + UInt32 + UInt64 + UInt128 + UInt256 + _ + + // Word* + _ + Word8 + Word16 + Word32 + Word64 + _ // future: Word128 + _ // future: Word256 + _ + + // Fix* + _ + _ // future: Fix8 + _ // future: Fix16 + _ // future: Fix32 + Fix64 + _ // future: Fix128 + _ // future: Fix256 + _ + + // UFix* + _ + _ // future: UFix8 + _ // future: UFix16 + _ // future: UFix32 + UFix64 + _ // future: UFix128 + _ // future: UFix256 +) + +func FromSemaType(ty sema.Type) ConstantKind { + switch ty { + // Int* + case sema.IntType, sema.IntegerType: + return Int + case sema.Int8Type: + return Int8 + case sema.Int16Type: + return Int16 + case sema.Int32Type: + return Int32 + case sema.Int64Type: + return Int64 + case sema.Int128Type: + return Int128 + case sema.Int256Type: + return Int256 + + // UInt* + case sema.UIntType: + return UInt + case sema.UInt8Type: + return UInt8 + case sema.UInt16Type: + return UInt16 + case sema.UInt32Type: + return UInt32 + case sema.UInt64Type: + return UInt64 + case sema.UInt128Type: + return UInt128 + case sema.UInt256Type: + return UInt256 + + // Word* + case sema.Word8Type: + return Word8 + case sema.Word16Type: + return Word16 + case sema.Word32Type: + return Word32 + case sema.Word64Type: + return Word64 + + // Fix* + case sema.Fix64Type: + return Fix64 + case sema.UFix64Type: + return UFix64 + + default: + panic(errors.NewUnreachableError()) + } +} diff --git a/bbq/constantkind/constantkind_string.go b/bbq/constantkind/constantkind_string.go new file mode 100644 index 0000000000..c1b31f2c13 --- /dev/null +++ b/bbq/constantkind/constantkind_string.go @@ -0,0 +1,66 @@ +// Code generated by "stringer -type=ConstantKind"; DO NOT EDIT. + +package constantkind + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[Unknown-0] + _ = x[String-1] + _ = x[Int-2] + _ = x[Int8-3] + _ = x[Int16-4] + _ = x[Int32-5] + _ = x[Int64-6] + _ = x[Int128-7] + _ = x[Int256-8] + _ = x[UInt-10] + _ = x[UInt8-11] + _ = x[UInt16-12] + _ = x[UInt32-13] + _ = x[UInt64-14] + _ = x[UInt128-15] + _ = x[UInt256-16] + _ = x[Word8-19] + _ = x[Word16-20] + _ = x[Word32-21] + _ = x[Word64-22] + _ = x[Fix64-30] + _ = x[UFix64-38] +} + +const ( + _ConstantKind_name_0 = "UnknownStringIntInt8Int16Int32Int64Int128Int256" + _ConstantKind_name_1 = "UIntUInt8UInt16UInt32UInt64UInt128UInt256" + _ConstantKind_name_2 = "Word8Word16Word32Word64" + _ConstantKind_name_3 = "Fix64" + _ConstantKind_name_4 = "UFix64" +) + +var ( + _ConstantKind_index_0 = [...]uint8{0, 7, 13, 16, 20, 25, 30, 35, 41, 47} + _ConstantKind_index_1 = [...]uint8{0, 4, 9, 15, 21, 27, 34, 41} + _ConstantKind_index_2 = [...]uint8{0, 5, 11, 17, 23} +) + +func (i ConstantKind) String() string { + switch { + case i <= 8: + return _ConstantKind_name_0[_ConstantKind_index_0[i]:_ConstantKind_index_0[i+1]] + case 10 <= i && i <= 16: + i -= 10 + return _ConstantKind_name_1[_ConstantKind_index_1[i]:_ConstantKind_index_1[i+1]] + case 19 <= i && i <= 22: + i -= 19 + return _ConstantKind_name_2[_ConstantKind_index_2[i]:_ConstantKind_index_2[i+1]] + case i == 30: + return _ConstantKind_name_3 + case i == 38: + return _ConstantKind_name_4 + default: + return "ConstantKind(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/bbq/contract.go b/bbq/contract.go new file mode 100644 index 0000000000..4c1aa7ed9a --- /dev/null +++ b/bbq/contract.go @@ -0,0 +1,25 @@ +/* + * 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 bbq + +type Contract struct { + Name string + Address []byte + IsInterface bool +} diff --git a/bbq/function.go b/bbq/function.go new file mode 100644 index 0000000000..cc58cffaae --- /dev/null +++ b/bbq/function.go @@ -0,0 +1,29 @@ +/* + * 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 bbq + +type Function[E any] struct { + Name string + Code []E + ParameterCount uint16 + TypeParameterCount uint16 + LocalCount uint16 + + IsCompositeFunction bool +} diff --git a/bbq/import.go b/bbq/import.go new file mode 100644 index 0000000000..4e54052967 --- /dev/null +++ b/bbq/import.go @@ -0,0 +1,26 @@ +/* + * 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 bbq + +import "github.com/onflow/cadence/common" + +type Import struct { + Location common.Location + Name string +} diff --git a/bbq/leb128/leb128.go b/bbq/leb128/leb128.go new file mode 100644 index 0000000000..80469c96bc --- /dev/null +++ b/bbq/leb128/leb128.go @@ -0,0 +1,220 @@ +/* + * 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 leb128 + +import ( + "fmt" +) + +// max32bitByteCount is the maximum number of bytes a 32-bit integer +// (signed or unsigned) may be encoded as. From +// https://webassembly.github.io/spec/core/binary/values.html#binary-int: +// +// "the total number of bytes encoding a value of type uN must not exceed ceil(N/7) bytes" +// "the total number of bytes encoding a value of type sN must not exceed ceil(N/7) bytes" +const max32bitByteCount = 5 + +// max64bitByteCount is the maximum number of bytes a 64-bit integer +// (signed or unsigned) may be encoded as. From +// https://webassembly.github.io/spec/core/binary/values.html#binary-int: +// +// "the total number of bytes encoding a value of type uN must not exceed ceil(N/7) bytes" +// "the total number of bytes encoding a value of type sN must not exceed ceil(N/7) bytes" +const max64bitByteCount = 10 + +// AppendUint32 encodes and writes the given unsigned 32-bit integer +// in canonical (with the fewest bytes possible) unsigned little-endian base-128 format +func AppendUint32(data []byte, v uint32) []byte { + if v < 128 { + data = append(data, uint8(v)) + return data + } + + more := true + for more { + // low order 7 bits of value + c := uint8(v & 0x7f) + v >>= 7 + // more bits to come? + more = v != 0 + if more { + // set high order bit of byte + c |= 0x80 + } + data = append(data, c) + } + return data +} + +// AppendUint64 encodes and writes the given unsigned 64-bit integer +// in canonical (with the fewest bytes possible) unsigned little-endian base-128 format +func AppendUint64(data []byte, v uint64) []byte { + if v < 128 { + data = append(data, uint8(v)) + return data + } + + more := true + for more { + // low order 7 bits of value + c := uint8(v & 0x7f) + v >>= 7 + // more bits to come? + more = v != 0 + if more { + // set high order bit of byte + c |= 0x80 + } + // emit byte + data = append(data, c) + } + return data +} + +// AppendUint32FixedLength encodes and writes the given unsigned 32-bit integer +// in non-canonical (fixed-size, instead of with the fewest bytes possible) +// unsigned little-endian base-128 format +func AppendUint32FixedLength(data []byte, v uint32, length int) ([]byte, error) { + for i := 0; i < length; i++ { + c := uint8(v & 0x7f) + v >>= 7 + if i < length-1 { + c |= 0x80 + } + data = append(data, c) + } + if v != 0 { + return nil, fmt.Errorf("length too small: %d", length) + } + return data, nil +} + +// ReadUint32 reads and decodes an unsigned 32-bit integer +func ReadUint32(data []byte) (result uint32, count int, err error) { + var shift uint + // only read up to maximum number of bytes + for i := 0; i < max32bitByteCount; i++ { + if i >= len(data) { + return 0, 0, fmt.Errorf("data too short: %d", len(data)) + } + b := data[i] + count++ + result |= (uint32(b & 0x7F)) << shift + // check high order bit of byte + if b&0x80 == 0 { + break + } + shift += 7 + } + return result, count, nil +} + +// ReadUint64 reads and decodes an unsigned 64-bit integer +func ReadUint64(data []byte) (result uint64, count int, err error) { + var shift uint + // only read up to maximum number of bytes + for i := 0; i < max64bitByteCount; i++ { + if i >= len(data) { + return 0, 0, fmt.Errorf("data too short: %d", len(data)) + } + b := data[i] + count++ + result |= (uint64(b & 0x7F)) << shift + // check high order bit of byte + if b&0x80 == 0 { + break + } + shift += 7 + } + return result, count, nil +} + +// AppendInt32 encodes and writes the given signed 32-bit integer +// in canonical (with the fewest bytes possible) signed little-endian base-128 format +func AppendInt32(data []byte, v int32) []byte { + more := true + for more { + // low order 7 bits of value + c := uint8(v & 0x7f) + sign := uint8(v & 0x40) + v >>= 7 + more = !((v == 0 && sign == 0) || (v == -1 && sign != 0)) + if more { + c |= 0x80 + } + data = append(data, c) + } + return data +} + +// AppendInt64 encodes and writes the given signed 64-bit integer +// in canonical (with the fewest bytes possible) signed little-endian base-128 format +func AppendInt64(data []byte, v int64) []byte { + more := true + for more { + // low order 7 bits of value + c := uint8(v & 0x7f) + sign := uint8(v & 0x40) + v >>= 7 + more = !((v == 0 && sign == 0) || (v == -1 && sign != 0)) + if more { + c |= 0x80 + } + data = append(data, c) + } + return data +} + +// ReadInt32 reads and decodes a signed 32-bit integer +func ReadInt32(data []byte) (result int32, count int, err error) { + var b byte = 0x80 + var signBits int32 = -1 + for i := 0; (b&0x80 == 0x80) && i < max32bitByteCount; i++ { + if i >= len(data) { + return 0, 0, fmt.Errorf("data too short: %d", len(data)) + } + b = data[i] + count++ + result += int32(b&0x7f) << (i * 7) + signBits <<= 7 + } + if ((signBits >> 1) & result) != 0 { + result += signBits + } + return result, count, nil +} + +// ReadInt64 reads and decodes a signed 64-bit integer +func ReadInt64(data []byte) (result int64, count int, err error) { + var b byte = 0x80 + var signBits int64 = -1 + for i := 0; (b&0x80 == 0x80) && i < max64bitByteCount; i++ { + if i >= len(data) { + return 0, 0, fmt.Errorf("data too short: %d", len(data)) + } + b = data[i] + count++ + result += int64(b&0x7f) << (i * 7) + signBits <<= 7 + } + if ((signBits >> 1) & result) != 0 { + result += signBits + } + return result, count, nil +} diff --git a/bbq/leb128/leb128_test.go b/bbq/leb128/leb128_test.go new file mode 100644 index 0000000000..ac0fa0be15 --- /dev/null +++ b/bbq/leb128/leb128_test.go @@ -0,0 +1,292 @@ +/* + * 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 leb128 + +import ( + "math" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestUint32(t *testing.T) { + + t.Parallel() + + t.Run("DWARF spec + more", func(t *testing.T) { + + t.Parallel() + + // DWARF Debugging Information Format, Version 3, page 140 + + for v, expected := range map[uint32][]byte{ + 0: {0x00}, + 1: {0x01}, + 2: {2}, + 63: {0x3f}, + 64: {0x40}, + 127: {127}, + 128: {0 + 0x80, 1}, + 129: {1 + 0x80, 1}, + 130: {2 + 0x80, 1}, + 0x90: {0x90, 0x01}, + 0x100: {0x80, 0x02}, + 0x101: {0x81, 0x02}, + 0xff: {0xff, 0x01}, + 12857: {57 + 0x80, 100}, + } { + var b []byte + b = AppendUint32(b, v) + require.Equal(t, expected, b) + + actual, n, err := ReadUint32(b) + require.NoError(t, err) + require.Equal(t, v, actual) + require.Equal(t, len(b), n) + } + }) + + t.Run("write: max byte count", func(t *testing.T) { + + t.Parallel() + + // This test ensures that only up to the maximum number of bytes are written + // when writing a LEB128-encoded 32-bit number (see max32bitByteCount), + // i.e. test that only up to 5 bytes are written. + + var b []byte + AppendUint32(b, math.MaxUint32) + require.GreaterOrEqual(t, max32bitByteCount, len(b)) + }) + + t.Run("read: max byte count", func(t *testing.T) { + + t.Parallel() + + // This test ensures that only up to the maximum number of bytes are read + // when reading a LEB128-encoded 32-bit number (see max32bitByteCount), + // i.e. test that only 5 of the 8 given bytes are read, + // to ensure the LEB128 parser doesn't keep reading infinitely. + + b := []byte{0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88} + _, n, err := ReadUint32(b) + require.NoError(t, err) + require.Equal(t, max32bitByteCount, n) + }) +} + +func TestBuf_Uint64LEB128(t *testing.T) { + + t.Parallel() + + t.Run("DWARF spec + more", func(t *testing.T) { + + t.Parallel() + + // DWARF Debugging Information Format, Version 3, page 140 + + for v, expected := range map[uint64][]byte{ + 0: {0x00}, + 1: {0x01}, + 2: {2}, + 63: {0x3f}, + 64: {0x40}, + 127: {127}, + 128: {0 + 0x80, 1}, + 129: {1 + 0x80, 1}, + 130: {2 + 0x80, 1}, + 0x90: {0x90, 0x01}, + 0x100: {0x80, 0x02}, + 0x101: {0x81, 0x02}, + 0xff: {0xff, 0x01}, + 12857: {57 + 0x80, 100}, + } { + var b []byte + b = AppendUint64(b, v) + require.Equal(t, expected, b) + + actual, n, err := ReadUint64(b) + require.NoError(t, err) + require.Equal(t, v, actual) + require.Equal(t, len(b), n) + } + }) + + t.Run("write: max byte count", func(t *testing.T) { + + t.Parallel() + + var b []byte + b = AppendUint64(b, math.MaxUint64) + require.GreaterOrEqual(t, max64bitByteCount, len(b)) + }) + + t.Run("read: max byte count", func(t *testing.T) { + + t.Parallel() + + b := []byte{ + 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, + 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f, 0x90, + } + _, n, err := ReadUint64(b) + require.NoError(t, err) + require.Equal(t, max64bitByteCount, n) + }) +} + +func TestBuf_Int32(t *testing.T) { + + t.Parallel() + + t.Run("DWARF spec + more", func(t *testing.T) { + + t.Parallel() + + // DWARF Debugging Information Format, Version 3, page 141 + + for v, expected := range map[int32][]byte{ + 0: {0x00}, + 1: {0x01}, + -1: {0x7f}, + 2: {2}, + -2: {0x7e}, + 63: {0x3f}, + -63: {0x41}, + 64: {0xc0, 0x00}, + -64: {0x40}, + -65: {0xbf, 0x7f}, + 127: {127 + 0x80, 0}, + -127: {1 + 0x80, 0x7f}, + 128: {0 + 0x80, 1}, + -128: {0 + 0x80, 0x7f}, + 129: {1 + 0x80, 1}, + -129: {0x7f + 0x80, 0x7e}, + -12345: {0xc7, 0x9f, 0x7f}, + } { + var b []byte + b = AppendInt32(b, v) + require.Equal(t, expected, b) + + actual, n, err := ReadInt32(b) + require.NoError(t, err) + require.Equal(t, v, actual) + require.Equal(t, len(b), n) + } + }) + + t.Run("write: max byte count", func(t *testing.T) { + + t.Parallel() + + // This test ensures that only up to the maximum number of bytes are written + // when writing a LEB128-encoded 32-bit number (see max32bitByteCount), + // i.e. test that only up to 5 bytes are written. + + var b []byte + b = AppendInt32(b, math.MaxInt32) + require.GreaterOrEqual(t, max32bitByteCount, len(b)) + + var b2 []byte + b2 = AppendInt32(b2, math.MinInt32) + require.GreaterOrEqual(t, max32bitByteCount, len(b2)) + }) + + t.Run("read: max byte count", func(t *testing.T) { + + t.Parallel() + + // This test ensures that only up to the maximum number of bytes are read + // when reading a LEB128-encoded 32-bit number (see max32bitByteCount), + // i.e. test that only 5 of the 8 given bytes are read, + // to ensure the LEB128 parser doesn't keep reading infinitely. + + b := []byte{0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88} + _, n, err := ReadInt32(b) + require.NoError(t, err) + require.Equal(t, max32bitByteCount, n) + }) +} + +func TestBuf_Int64LEB128(t *testing.T) { + + t.Parallel() + + t.Run("DWARF spec + more", func(t *testing.T) { + + t.Parallel() + + // DWARF Debugging Information Format, Version 3, page 141 + + for v, expected := range map[int64][]byte{ + 0: {0x00}, + 1: {0x01}, + -1: {0x7f}, + 2: {2}, + -2: {0x7e}, + 63: {0x3f}, + -63: {0x41}, + 64: {0xc0, 0x00}, + -64: {0x40}, + -65: {0xbf, 0x7f}, + 127: {127 + 0x80, 0}, + -127: {1 + 0x80, 0x7f}, + 128: {0 + 0x80, 1}, + -128: {0 + 0x80, 0x7f}, + 129: {1 + 0x80, 1}, + -129: {0x7f + 0x80, 0x7e}, + -12345: {0xc7, 0x9f, 0x7f}, + } { + var b []byte + b = AppendInt64(b, v) + require.Equal(t, expected, b) + + actual, n, err := ReadInt64(b) + require.NoError(t, err) + require.Equal(t, v, actual) + require.Equal(t, len(b), n) + } + }) + + t.Run("write: max byte count", func(t *testing.T) { + + t.Parallel() + + var b []byte + b = AppendInt64(b, math.MaxInt64) + require.GreaterOrEqual(t, max64bitByteCount, len(b)) + + var b2 []byte + b2 = AppendInt64(b2, math.MinInt64) + require.GreaterOrEqual(t, max64bitByteCount, len(b2)) + }) + + t.Run("read: max byte count", func(t *testing.T) { + + t.Parallel() + + b := []byte{ + 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, + 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f, 0x90, + } + _, n, err := ReadInt64(b) + require.NoError(t, err) + require.Equal(t, max64bitByteCount, n) + }) +} diff --git a/bbq/opcode/castkind.go b/bbq/opcode/castkind.go new file mode 100644 index 0000000000..a2ec015457 --- /dev/null +++ b/bbq/opcode/castkind.go @@ -0,0 +1,45 @@ +/* + * 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/gen/instructions.schema.json b/bbq/opcode/gen/instructions.schema.json new file mode 100644 index 0000000000..62c8bd9683 --- /dev/null +++ b/bbq/opcode/gen/instructions.schema.json @@ -0,0 +1,129 @@ +{ + "type": "array", + "items": { "$ref": "#/$defs/instruction" }, + "$defs": { + "instruction": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "description": { "type": "string" }, + "operands": { + "type": "array", + "items": { "$ref": "#/$defs/operand" } + }, + "valueEffects": {"$ref": "#/$defs/valueEffects"}, + "controlEffects": {"$ref": "#/$defs/controlEffects"} + }, + "required": ["name", "description"], + "additionalProperties": false + }, + "operand": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "type": { + "type": "string", + "enum": [ + "bool", + "index", + "indices", + "size", + "string", + "castKind", + "pathDomain", + "compositeKind" + ] + }, + "description": { "type": "string" } + }, + "required": ["name", "type"], + "additionalProperties": false + }, + "valueEffects": { + "type": "object", + "properties": { + "push": { + "type": "array", + "items": { "$ref": "#/$defs/valueStackOp" } + }, + "pop": { + "type": "array", + "items": { + "$ref": "#/$defs/valueStackOp" + } + } + }, + "additionalProperties": false + }, + "valueStackOp": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "type": { + "type": "string", + "enum": [ + "value", + "string", + "array", + "integer", + "bool", + "path", + "int", + "reference", + "function", + "resource", + "optional" + ] + }, + "count": { + "oneOf": [ + { "type": "integer" }, + { "type": "string" } + ] + } + }, + "required": ["name", "type"], + "additionalProperties": false + }, + "controlEffects": { + "type": "array", + "items": { "$ref": "#/$defs/controlEffect" } + }, + "controlEffect": { + "oneOf": [ + { "$ref": "#/$defs/jump" }, + { "$ref": "#/$defs/call" }, + { "$ref": "#/$defs/return" } + ] + }, + "jump": { + "type": "object", + "properties": { + "jump": { "type": "string" } + }, + "required": ["jump"], + "additionalProperties": false + }, + "call": { + "type": "object", + "properties": { + "call": { "type": "null" } + }, + "required": ["call"], + "additionalProperties": false + }, + "return": { + "type": "object", + "properties": { + "return": { + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] + } + }, + "required": ["return"], + "additionalProperties": false + } + } +} \ No newline at end of file diff --git a/bbq/opcode/gen/main.go b/bbq/opcode/gen/main.go new file mode 100644 index 0000000000..2a3d1f2ec4 --- /dev/null +++ b/bbq/opcode/gen/main.go @@ -0,0 +1,832 @@ +package main + +import ( + "bytes" + "fmt" + "go/token" + "io" + "os" + "unicode" + "unicode/utf8" + + "github.com/dave/dst" + "github.com/dave/dst/decorator" + "github.com/dave/dst/decorator/resolver/guess" + "github.com/goccy/go-yaml" +) + +const ( + opcodePackagePath = "github.com/onflow/cadence/bbq/opcode" + commonPackagePath = "github.com/onflow/cadence/common" + errorsPackagePath = "github.com/onflow/cadence/errors" +) + +const headerFormat = `// Code generated by gen/main.go from %s. DO NOT EDIT. + +` + +const typeCommentFormat = `// %s +// +// %s +` + +type operandType string + +const ( + operandTypeBool = "bool" + operandTypeIndex = "index" + operandTypeIndices = "indices" + operandTypeSize = "size" + operandTypeString = "string" + operandTypeCastKind = "castKind" + operandTypePathDomain = "pathDomain" + operandTypeCompositeKind = "compositeKind" +) + +type instruction struct { + Name string + Description string + Operands []operand +} + +type operand struct { + Name string + Description string + Type operandType +} + +func main() { + if len(os.Args) != 3 { + panic("usage: gen instructions.yml instructions.go") + } + + yamlPath := os.Args[1] + goPath := os.Args[2] + + yamlContents, err := os.ReadFile(yamlPath) + if err != nil { + panic(err) + } + + var instructions []instruction + + err = yaml.Unmarshal(yamlContents, &instructions) + if err != nil { + panic(err) + } + + var buffer bytes.Buffer + _, err = fmt.Fprintf(&buffer, headerFormat, yamlPath) + if err != nil { + panic(err) + } + + var decls []dst.Decl + for _, instruction := range instructions { + decls = append( + decls, + instructionDecls(instruction)..., + ) + } + decls = append(decls, decodeInstructionFuncDecl(instructions)) + + writeGoFile(&buffer, decls, opcodePackagePath) + + err = os.WriteFile(goPath, buffer.Bytes(), 0644) + if err != nil { + panic(err) + } +} + +func decodeInstructionFuncDecl(instructions []instruction) *dst.FuncDecl { + + var caseStmts []dst.Stmt + + for _, ins := range instructions { + + var resultExpr dst.Expr + if len(ins.Operands) == 0 { + resultExpr = &dst.CompositeLit{ + Type: instructionIdent(ins), + } + } else { + resultExpr = &dst.CallExpr{ + Fun: &dst.Ident{ + Name: "Decode" + firstUpper(ins.Name), + }, + Args: []dst.Expr{ + dst.NewIdent("ip"), + dst.NewIdent("code"), + }, + } + } + + caseStmts = append( + caseStmts, + &dst.CaseClause{ + List: []dst.Expr{ + dst.NewIdent(firstUpper(ins.Name)), + }, + Body: []dst.Stmt{ + &dst.ReturnStmt{ + Results: []dst.Expr{ + resultExpr, + }, + }, + }, + }, + ) + } + + switchStmt := &dst.SwitchStmt{ + Tag: &dst.CallExpr{ + Fun: &dst.Ident{ + Name: "Opcode", + Path: opcodePackagePath, + }, + Args: []dst.Expr{ + &dst.CallExpr{ + + Fun: &dst.Ident{ + Name: "decodeByte", + Path: opcodePackagePath, + }, + Args: []dst.Expr{ + dst.NewIdent("ip"), + dst.NewIdent("code"), + }, + }, + }, + }, + Body: &dst.BlockStmt{ + List: caseStmts, + }, + Decs: dst.SwitchStmtDecorations{ + NodeDecs: dst.NodeDecs{ + Before: dst.NewLine, + After: dst.NewLine, + }, + }, + } + + stmts := []dst.Stmt{ + switchStmt, + &dst.ExprStmt{ + X: &dst.CallExpr{ + Fun: dst.NewIdent("panic"), + Args: []dst.Expr{ + &dst.CallExpr{ + Fun: &dst.Ident{ + Name: "NewUnreachableError", + Path: errorsPackagePath, + }, + }, + }, + }, + Decs: dst.ExprStmtDecorations{ + NodeDecs: dst.NodeDecs{ + Before: dst.EmptyLine, + After: dst.NewLine, + }, + }, + }, + } + + return &dst.FuncDecl{ + Name: dst.NewIdent("DecodeInstruction"), + Type: &dst.FuncType{ + Params: &dst.FieldList{ + List: []*dst.Field{ + { + Names: []*dst.Ident{ + dst.NewIdent("ip"), + }, + Type: &dst.StarExpr{ + X: dst.NewIdent("uint16"), + }, + }, + { + Names: []*dst.Ident{ + dst.NewIdent("code"), + }, + Type: &dst.ArrayType{ + Elt: dst.NewIdent("byte"), + }, + }, + }, + }, + Results: &dst.FieldList{ + List: []*dst.Field{ + { + Type: &dst.Ident{ + Name: "Instruction", + Path: opcodePackagePath, + }, + }, + }, + }, + }, + Body: &dst.BlockStmt{ + List: stmts, + }, + } +} + +func instructionDecls(instruction instruction) []dst.Decl { + decls := []dst.Decl{ + instructionTypeDecl(instruction), + instructionConformanceDecl(instruction), + instructionOpcodeFuncDecl(instruction), + instructionStringFuncDecl(instruction), + instructionEncodeFuncDecl(instruction), + } + if len(instruction.Operands) > 0 { + decls = append(decls, instructionDecodeFuncDecl(instruction)) + } + return decls +} + +func instructionIdent(ins instruction) *dst.Ident { + return dst.NewIdent("Instruction" + firstUpper(ins.Name)) +} + +func operandIdent(o operand) *dst.Ident { + return dst.NewIdent(firstUpper(o.Name)) +} + +func instructionTypeDecl(ins instruction) dst.Decl { + comment := fmt.Sprintf( + typeCommentFormat, + instructionIdent(ins).String(), + ins.Description, + ) + + return &dst.GenDecl{ + Tok: token.TYPE, + Specs: []dst.Spec{ + &dst.TypeSpec{ + Name: instructionIdent(ins), + Type: &dst.StructType{ + Fields: instructionOperandsFields(ins), + }, + }, + }, + Decs: dst.GenDeclDecorations{ + NodeDecs: dst.NodeDecs{ + Start: []string{comment}, + }, + }, + } +} + +func instructionOperandsFields(ins instruction) *dst.FieldList { + fields := make([]*dst.Field, len(ins.Operands)) + for i, operand := range ins.Operands { + + var typeExpr dst.Expr + + switch operand.Type { + case operandTypeBool: + typeExpr = dst.NewIdent("bool") + + case operandTypeIndex, + operandTypeSize: + + typeExpr = dst.NewIdent("uint16") + + case operandTypeIndices: + typeExpr = &dst.ArrayType{ + Elt: dst.NewIdent("uint16"), + } + + case operandTypeString: + typeExpr = dst.NewIdent("string") + + case operandTypeCastKind: + typeExpr = &dst.Ident{ + Name: "CastKind", + Path: opcodePackagePath, + } + + case operandTypePathDomain: + typeExpr = &dst.Ident{ + Name: "PathDomain", + Path: commonPackagePath, + } + + case operandTypeCompositeKind: + typeExpr = &dst.Ident{ + Name: "CompositeKind", + Path: commonPackagePath, + } + + default: + panic(fmt.Sprintf("unsupported operand type: %s", operand.Type)) + } + + fields[i] = &dst.Field{ + Names: []*dst.Ident{ + operandIdent(operand), + }, + Type: typeExpr, + } + } + return &dst.FieldList{ + List: fields, + } +} + +func instructionConformanceDecl(ins instruction) *dst.GenDecl { + return &dst.GenDecl{ + Tok: token.VAR, + Specs: []dst.Spec{ + &dst.ValueSpec{ + Names: []*dst.Ident{ + dst.NewIdent("_"), + }, + Type: &dst.Ident{ + Name: "Instruction", + Path: opcodePackagePath, + }, + Values: []dst.Expr{ + &dst.CompositeLit{ + Type: instructionIdent(ins), + }, + }, + }, + }, + } +} + +func instructionOpcodeFuncDecl(ins instruction) *dst.FuncDecl { + stmt := &dst.ReturnStmt{ + Results: []dst.Expr{ + dst.NewIdent(firstUpper(ins.Name)), + }, + Decs: dst.ReturnStmtDecorations{ + NodeDecs: dst.NodeDecs{ + Before: dst.NewLine, + After: dst.NewLine, + }, + }, + } + return &dst.FuncDecl{ + Recv: &dst.FieldList{ + List: []*dst.Field{ + { + Type: instructionIdent(ins), + }, + }, + }, + Name: dst.NewIdent("Opcode"), + Type: &dst.FuncType{ + Params: &dst.FieldList{}, + Results: &dst.FieldList{ + List: []*dst.Field{ + { + Type: &dst.Ident{ + Name: "Opcode", + Path: opcodePackagePath, + }, + }, + }, + }, + }, + Body: &dst.BlockStmt{ + List: []dst.Stmt{ + stmt, + }, + }, + } +} + +func instructionStringFuncDecl(ins instruction) *dst.FuncDecl { + + var stmts []dst.Stmt + + opcodeStringExpr := &dst.CallExpr{ + Fun: &dst.SelectorExpr{ + X: &dst.CallExpr{ + Fun: &dst.SelectorExpr{ + X: dst.NewIdent("i"), + Sel: dst.NewIdent("Opcode"), + }, + }, + Sel: dst.NewIdent("String"), + }, + } + + if len(ins.Operands) == 0 { + stmt := &dst.ReturnStmt{ + Results: []dst.Expr{ + opcodeStringExpr, + }, + Decs: dst.ReturnStmtDecorations{ + NodeDecs: dst.NodeDecs{ + Before: dst.NewLine, + After: dst.NewLine, + }, + }, + } + stmts = append(stmts, stmt) + } else { + + stmts = append( + stmts, + &dst.DeclStmt{ + Decl: &dst.GenDecl{ + Tok: token.VAR, + Specs: []dst.Spec{ + &dst.ValueSpec{ + Names: []*dst.Ident{ + dst.NewIdent("sb"), + }, + Type: &dst.Ident{ + Name: "Builder", + Path: "strings", + }, + }, + }, + }, + Decs: dst.DeclStmtDecorations{ + NodeDecs: dst.NodeDecs{ + Before: dst.NewLine, + After: dst.NewLine, + }, + }, + }, + ) + + stmts = append( + stmts, + &dst.ExprStmt{ + X: &dst.CallExpr{ + Fun: &dst.SelectorExpr{ + X: dst.NewIdent("sb"), + Sel: dst.NewIdent("WriteString"), + }, + Args: []dst.Expr{ + opcodeStringExpr, + }, + }, + }, + ) + + for _, operand := range ins.Operands { + var funcName string + switch operand.Type { + case operandTypeIndices: + funcName = "printfUInt16ArrayArgument" + default: + funcName = "printfArgument" + } + stmts = append( + stmts, + &dst.ExprStmt{ + X: &dst.CallExpr{ + Fun: &dst.Ident{ + Name: funcName, + }, + Args: []dst.Expr{ + &dst.UnaryExpr{ + Op: token.AND, + X: dst.NewIdent("sb"), + }, + &dst.BasicLit{ + Kind: token.STRING, + Value: fmt.Sprintf(`"%s"`, operand.Name), + }, + &dst.SelectorExpr{ + X: dst.NewIdent("i"), + Sel: operandIdent(operand), + }, + }, + }, + }, + ) + } + + stmts = append( + stmts, + &dst.ReturnStmt{ + Results: []dst.Expr{ + &dst.CallExpr{ + Fun: &dst.SelectorExpr{ + X: dst.NewIdent("sb"), + Sel: dst.NewIdent("String"), + }, + }, + }, + Decs: dst.ReturnStmtDecorations{ + NodeDecs: dst.NodeDecs{ + Before: dst.NewLine, + After: dst.NewLine, + }, + }, + }, + ) + } + + return &dst.FuncDecl{ + Recv: &dst.FieldList{ + List: []*dst.Field{ + { + Names: []*dst.Ident{ + dst.NewIdent("i"), + }, + Type: instructionIdent(ins), + }, + }, + }, + Name: dst.NewIdent("String"), + Type: &dst.FuncType{ + Params: &dst.FieldList{}, + Results: &dst.FieldList{ + List: []*dst.Field{ + { + Type: dst.NewIdent("string"), + }, + }, + }, + }, + Body: &dst.BlockStmt{ + List: stmts, + }, + } +} + +func instructionEncodeFuncDecl(ins instruction) *dst.FuncDecl { + stmts := []dst.Stmt{ + &dst.ExprStmt{ + X: &dst.CallExpr{ + Fun: &dst.Ident{ + Name: "emitOpcode", + Path: opcodePackagePath, + }, + Args: []dst.Expr{ + dst.NewIdent("code"), + &dst.CallExpr{ + Fun: &dst.SelectorExpr{ + X: dst.NewIdent("i"), + Sel: dst.NewIdent("Opcode"), + }, + }, + }, + }, + Decs: dst.ExprStmtDecorations{ + NodeDecs: dst.NodeDecs{ + Before: dst.NewLine, + After: dst.NewLine, + }, + }, + }, + } + + for _, operand := range ins.Operands { + + var funcName string + + switch operand.Type { + case operandTypeBool: + funcName = "emitBool" + + case operandTypeIndex: + funcName = "emitUint16" + + case operandTypeIndices: + funcName = "emitUint16Array" + + case operandTypeSize: + funcName = "emitUint16" + + case operandTypeString: + funcName = "emitString" + + case operandTypeCastKind: + funcName = "emitCastKind" + + case operandTypePathDomain: + funcName = "emitPathDomain" + + case operandTypeCompositeKind: + funcName = "emitCompositeKind" + + default: + panic(fmt.Sprintf("unsupported operand type: %s", operand.Type)) + } + + stmts = append(stmts, + &dst.ExprStmt{ + X: &dst.CallExpr{ + Fun: &dst.Ident{ + Name: funcName, + Path: opcodePackagePath, + }, + Args: []dst.Expr{ + dst.NewIdent("code"), + &dst.SelectorExpr{ + X: dst.NewIdent("i"), + Sel: operandIdent(operand), + }, + }, + }, + Decs: dst.ExprStmtDecorations{ + NodeDecs: dst.NodeDecs{ + Before: dst.NewLine, + After: dst.NewLine, + }, + }, + }, + ) + } + + return &dst.FuncDecl{ + Recv: &dst.FieldList{ + List: []*dst.Field{ + { + Names: []*dst.Ident{ + dst.NewIdent("i"), + }, + Type: instructionIdent(ins), + }, + }, + }, + Name: dst.NewIdent("Encode"), + Type: &dst.FuncType{ + Params: &dst.FieldList{ + List: []*dst.Field{ + { + Names: []*dst.Ident{ + dst.NewIdent("code"), + }, + Type: &dst.StarExpr{ + X: &dst.ArrayType{ + Elt: dst.NewIdent("byte"), + }, + }, + }, + }, + }, + Results: &dst.FieldList{}, + }, + Body: &dst.BlockStmt{ + List: stmts, + }, + } +} + +func instructionDecodeFuncDecl(ins instruction) *dst.FuncDecl { + var stmts []dst.Stmt + + for _, operand := range ins.Operands { + + var funcName string + + switch operand.Type { + case operandTypeBool: + funcName = "decodeBool" + + case operandTypeIndex: + funcName = "decodeUint16" + + case operandTypeIndices: + funcName = "decodeUint16Array" + + case operandTypeSize: + funcName = "decodeUint16" + + case operandTypeString: + funcName = "decodeString" + + case operandTypeCastKind: + funcName = "decodeCastKind" + + case operandTypePathDomain: + funcName = "decodePathDomain" + + case operandTypeCompositeKind: + funcName = "decodeCompositeKind" + + default: + panic(fmt.Sprintf("unsupported operand type: %s", operand.Type)) + } + + stmts = append(stmts, + &dst.AssignStmt{ + Lhs: []dst.Expr{ + &dst.SelectorExpr{ + X: dst.NewIdent("i"), + Sel: operandIdent(operand), + }, + }, + Tok: token.ASSIGN, + Rhs: []dst.Expr{ + &dst.CallExpr{ + Fun: &dst.Ident{ + Name: funcName, + Path: opcodePackagePath, + }, + Args: []dst.Expr{ + dst.NewIdent("ip"), + dst.NewIdent("code"), + }, + }, + }, + Decs: dst.AssignStmtDecorations{ + NodeDecs: dst.NodeDecs{ + Before: dst.NewLine, + After: dst.NewLine, + }, + }, + }, + ) + } + + stmts = append( + stmts, + &dst.ReturnStmt{ + Results: []dst.Expr{ + dst.NewIdent("i"), + }, + Decs: dst.ReturnStmtDecorations{ + NodeDecs: dst.NodeDecs{ + Before: dst.NewLine, + After: dst.NewLine, + }, + }, + }, + ) + + return &dst.FuncDecl{ + Name: dst.NewIdent("Decode" + firstUpper(ins.Name)), + Type: &dst.FuncType{ + Params: &dst.FieldList{ + List: []*dst.Field{ + { + Names: []*dst.Ident{ + dst.NewIdent("ip"), + }, + Type: &dst.StarExpr{ + X: dst.NewIdent("uint16"), + }, + }, + { + Names: []*dst.Ident{ + dst.NewIdent("code"), + }, + Type: &dst.ArrayType{ + Elt: dst.NewIdent("byte"), + }, + }, + }, + }, + Results: &dst.FieldList{ + List: []*dst.Field{ + { + Names: []*dst.Ident{ + dst.NewIdent("i"), + }, + Type: instructionIdent(ins), + }, + }, + }, + }, + Body: &dst.BlockStmt{ + List: stmts, + }, + } +} + +func writeGoFile(writer io.Writer, decls []dst.Decl, packagePath string) { + resolver := guess.New() + restorer := decorator.NewRestorerWithImports(packagePath, resolver) + + packageName, err := resolver.ResolvePackage(packagePath) + if err != nil { + panic(err) + } + + for _, decl := range decls { + decl.Decorations().Before = dst.NewLine + decl.Decorations().After = dst.EmptyLine + } + + err = restorer.Fprint( + writer, + &dst.File{ + Name: dst.NewIdent(packageName), + Decls: decls, + }, + ) + if err != nil { + panic(err) + } +} + +func firstUpper(s string) string { + if s == "" { + return "" + } + r, n := utf8.DecodeRuneInString(s) + return string(unicode.ToUpper(r)) + s[n:] +} diff --git a/bbq/opcode/instruction.go b/bbq/opcode/instruction.go new file mode 100644 index 0000000000..9c69db2de6 --- /dev/null +++ b/bbq/opcode/instruction.go @@ -0,0 +1,184 @@ +/* + * 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. + */ + +//go:generate go run ./gen/main.go instructions.yml instructions.go + +package opcode + +import ( + "fmt" + "strings" + + "github.com/onflow/cadence/common" +) + +type Instruction interface { + Encode(code *[]byte) + String() string + Opcode() Opcode +} + +func emitOpcode(code *[]byte, opcode Opcode) { + *code = append(*code, byte(opcode)) +} + +// uint16 + +// encodeUint16 encodes the given uint16 in big-endian representation +func encodeUint16(v uint16) (byte, byte) { + return byte((v >> 8) & 0xff), + byte(v & 0xff) +} + +// emitUint16 encodes the given uint16 in big-endian representation +func emitUint16(code *[]byte, v uint16) { + first, last := encodeUint16(v) + *code = append(*code, first, last) +} + +func decodeUint16(ip *uint16, code []byte) uint16 { + first := code[*ip] + last := code[*ip+1] + *ip += 2 + return uint16(first)<<8 | uint16(last) +} + +// Byte + +func decodeByte(ip *uint16, code []byte) byte { + byt := code[*ip] + *ip += 1 + return byt +} + +func emitByte(code *[]byte, b byte) { + *code = append(*code, b) +} + +// Bool + +func decodeBool(ip *uint16, code []byte) bool { + return decodeByte(ip, code) == 1 +} + +func emitBool(code *[]byte, v bool) { + var b byte + if v { + b = 1 + } + *code = append(*code, b) +} + +// String + +func decodeString(ip *uint16, code []byte) string { + strLen := decodeUint16(ip, code) + start := *ip + *ip += strLen + end := *ip + return string(code[start:end]) +} + +func emitString(code *[]byte, str string) { + emitUint16(code, uint16(len(str))) + *code = append(*code, []byte(str)...) +} + +// PathDomain + +func decodePathDomain(ip *uint16, code []byte) common.PathDomain { + return common.PathDomain(decodeByte(ip, code)) +} + +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 { + return common.CompositeKind(decodeUint16(ip, code)) +} + +func emitCompositeKind(code *[]byte, kind common.CompositeKind) { + emitUint16(code, uint16(kind)) +} + +// Uint16Array + +func emitUint16Array(code *[]byte, values []uint16) { + emitUint16(code, uint16(len(values))) + for _, value := range values { + emitUint16(code, value) + } +} + +func decodeUint16Array(ip *uint16, code []byte) (values []uint16) { + typeArgCount := decodeUint16(ip, code) + for i := 0; i < int(typeArgCount); i++ { + value := decodeUint16(ip, code) + values = append(values, value) + } + return values +} + +// Jump + +func PatchJump(code *[]byte, opcodeOffset int, newTarget uint16) { + first, second := encodeUint16(newTarget) + (*code)[opcodeOffset+1] = first + (*code)[opcodeOffset+2] = second +} + +func DecodeInstructions(code []byte) []Instruction { + var instructions []Instruction + var ip uint16 + for ip < uint16(len(code)) { + instruction := DecodeInstruction(&ip, code) + instructions = append(instructions, instruction) + } + return instructions +} + +// Instruction pretty print + +func printfUInt16ArrayArgument(sb *strings.Builder, argName string, values []uint16) { + _, _ = fmt.Fprintf(sb, " %s:[", argName) + for i, value := range values { + if i > 0 { + _, _ = fmt.Fprint(sb, ", ") + } + _, _ = fmt.Fprintf(sb, "%d", value) + } + + sb.WriteString("]") +} + +func printfArgument(sb *strings.Builder, fieldName string, v any) { + _, _ = fmt.Fprintf(sb, " %s:%v", fieldName, v) +} diff --git a/bbq/opcode/instructions.go b/bbq/opcode/instructions.go new file mode 100644 index 0000000000..19ed6de1d5 --- /dev/null +++ b/bbq/opcode/instructions.go @@ -0,0 +1,1095 @@ +// Code generated by gen/main.go from instructions.yml. DO NOT EDIT. + +package opcode + +import ( + "strings" + + "github.com/onflow/cadence/common" + "github.com/onflow/cadence/errors" +) + +// InstructionUnknown +// +// An unknown instruction. +type InstructionUnknown struct { +} + +var _ Instruction = InstructionUnknown{} + +func (InstructionUnknown) Opcode() Opcode { + return Unknown +} + +func (i InstructionUnknown) String() string { + return i.Opcode().String() +} + +func (i InstructionUnknown) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) +} + +// InstructionGetLocal +// +// Pushes the value of the local at the given index onto the stack. +type InstructionGetLocal struct { + LocalIndex uint16 +} + +var _ Instruction = InstructionGetLocal{} + +func (InstructionGetLocal) Opcode() Opcode { + return GetLocal +} + +func (i InstructionGetLocal) String() string { + var sb strings.Builder + sb.WriteString(i.Opcode().String()) + printfArgument(&sb, "localIndex", i.LocalIndex) + return sb.String() +} + +func (i InstructionGetLocal) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) + emitUint16(code, i.LocalIndex) +} + +func DecodeGetLocal(ip *uint16, code []byte) (i InstructionGetLocal) { + i.LocalIndex = decodeUint16(ip, code) + return i +} + +// InstructionSetLocal +// +// Sets the value of the local at the given index to the top value on the stack. +type InstructionSetLocal struct { + LocalIndex uint16 +} + +var _ Instruction = InstructionSetLocal{} + +func (InstructionSetLocal) Opcode() Opcode { + return SetLocal +} + +func (i InstructionSetLocal) String() string { + var sb strings.Builder + sb.WriteString(i.Opcode().String()) + printfArgument(&sb, "localIndex", i.LocalIndex) + return sb.String() +} + +func (i InstructionSetLocal) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) + emitUint16(code, i.LocalIndex) +} + +func DecodeSetLocal(ip *uint16, code []byte) (i InstructionSetLocal) { + i.LocalIndex = decodeUint16(ip, code) + return i +} + +// InstructionGetGlobal +// +// Pushes the value of the global at the given index onto the stack. +type InstructionGetGlobal struct { + GlobalIndex uint16 +} + +var _ Instruction = InstructionGetGlobal{} + +func (InstructionGetGlobal) Opcode() Opcode { + return GetGlobal +} + +func (i InstructionGetGlobal) String() string { + var sb strings.Builder + sb.WriteString(i.Opcode().String()) + printfArgument(&sb, "globalIndex", i.GlobalIndex) + return sb.String() +} + +func (i InstructionGetGlobal) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) + emitUint16(code, i.GlobalIndex) +} + +func DecodeGetGlobal(ip *uint16, code []byte) (i InstructionGetGlobal) { + i.GlobalIndex = decodeUint16(ip, code) + return i +} + +// InstructionSetGlobal +// +// Sets the value of the global at the given index to the top value on the stack. +type InstructionSetGlobal struct { + GlobalIndex uint16 +} + +var _ Instruction = InstructionSetGlobal{} + +func (InstructionSetGlobal) Opcode() Opcode { + return SetGlobal +} + +func (i InstructionSetGlobal) String() string { + var sb strings.Builder + sb.WriteString(i.Opcode().String()) + printfArgument(&sb, "globalIndex", i.GlobalIndex) + return sb.String() +} + +func (i InstructionSetGlobal) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) + emitUint16(code, i.GlobalIndex) +} + +func DecodeSetGlobal(ip *uint16, code []byte) (i InstructionSetGlobal) { + i.GlobalIndex = decodeUint16(ip, code) + return i +} + +// InstructionGetField +// +// Pushes the value of the field at the given index onto the stack. +type InstructionGetField struct { + FieldNameIndex uint16 +} + +var _ Instruction = InstructionGetField{} + +func (InstructionGetField) Opcode() Opcode { + return GetField +} + +func (i InstructionGetField) String() string { + var sb strings.Builder + sb.WriteString(i.Opcode().String()) + printfArgument(&sb, "fieldNameIndex", i.FieldNameIndex) + return sb.String() +} + +func (i InstructionGetField) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) + emitUint16(code, i.FieldNameIndex) +} + +func DecodeGetField(ip *uint16, code []byte) (i InstructionGetField) { + i.FieldNameIndex = decodeUint16(ip, code) + return i +} + +// InstructionSetField +// +// Sets the value of the field at the given index to the top value on the stack. +type InstructionSetField struct { + FieldNameIndex uint16 +} + +var _ Instruction = InstructionSetField{} + +func (InstructionSetField) Opcode() Opcode { + return SetField +} + +func (i InstructionSetField) String() string { + var sb strings.Builder + sb.WriteString(i.Opcode().String()) + printfArgument(&sb, "fieldNameIndex", i.FieldNameIndex) + return sb.String() +} + +func (i InstructionSetField) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) + emitUint16(code, i.FieldNameIndex) +} + +func DecodeSetField(ip *uint16, code []byte) (i InstructionSetField) { + i.FieldNameIndex = decodeUint16(ip, code) + return i +} + +// InstructionGetIndex +// +// Pushes the value at the given index onto the stack. +type InstructionGetIndex struct { +} + +var _ Instruction = InstructionGetIndex{} + +func (InstructionGetIndex) Opcode() Opcode { + return GetIndex +} + +func (i InstructionGetIndex) String() string { + return i.Opcode().String() +} + +func (i InstructionGetIndex) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) +} + +// InstructionSetIndex +// +// Sets the value at the given index to the top value on the stack. +type InstructionSetIndex struct { +} + +var _ Instruction = InstructionSetIndex{} + +func (InstructionSetIndex) Opcode() Opcode { + return SetIndex +} + +func (i InstructionSetIndex) String() string { + return i.Opcode().String() +} + +func (i InstructionSetIndex) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) +} + +// InstructionTrue +// +// Pushes the boolean value `true` onto the stack. +type InstructionTrue struct { +} + +var _ Instruction = InstructionTrue{} + +func (InstructionTrue) Opcode() Opcode { + return True +} + +func (i InstructionTrue) String() string { + return i.Opcode().String() +} + +func (i InstructionTrue) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) +} + +// InstructionFalse +// +// Pushes the boolean value `false` onto the stack. +type InstructionFalse struct { +} + +var _ Instruction = InstructionFalse{} + +func (InstructionFalse) Opcode() Opcode { + return False +} + +func (i InstructionFalse) String() string { + return i.Opcode().String() +} + +func (i InstructionFalse) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) +} + +// InstructionNil +// +// Pushes the value `nil` onto the stack. +type InstructionNil struct { +} + +var _ Instruction = InstructionNil{} + +func (InstructionNil) Opcode() Opcode { + return Nil +} + +func (i InstructionNil) String() string { + return i.Opcode().String() +} + +func (i InstructionNil) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) +} + +// InstructionPath +// +// Pushes the path with the given domain and identifier onto the stack. +type InstructionPath struct { + Domain common.PathDomain + IdentifierIndex uint16 +} + +var _ Instruction = InstructionPath{} + +func (InstructionPath) Opcode() Opcode { + return Path +} + +func (i InstructionPath) String() string { + var sb strings.Builder + sb.WriteString(i.Opcode().String()) + printfArgument(&sb, "domain", i.Domain) + printfArgument(&sb, "identifierIndex", i.IdentifierIndex) + return sb.String() +} + +func (i InstructionPath) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) + emitPathDomain(code, i.Domain) + emitUint16(code, i.IdentifierIndex) +} + +func DecodePath(ip *uint16, code []byte) (i InstructionPath) { + i.Domain = decodePathDomain(ip, code) + i.IdentifierIndex = decodeUint16(ip, code) + return i +} + +// InstructionNew +// +// Creates a new instance of the given type. +type InstructionNew struct { + Kind common.CompositeKind + TypeIndex uint16 +} + +var _ Instruction = InstructionNew{} + +func (InstructionNew) Opcode() Opcode { + return New +} + +func (i InstructionNew) String() string { + var sb strings.Builder + sb.WriteString(i.Opcode().String()) + printfArgument(&sb, "kind", i.Kind) + printfArgument(&sb, "typeIndex", i.TypeIndex) + return sb.String() +} + +func (i InstructionNew) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) + emitCompositeKind(code, i.Kind) + emitUint16(code, i.TypeIndex) +} + +func DecodeNew(ip *uint16, code []byte) (i InstructionNew) { + i.Kind = decodeCompositeKind(ip, code) + i.TypeIndex = decodeUint16(ip, code) + return i +} + +// InstructionNewArray +// +// Creates a new array with the given type and size. +type InstructionNewArray struct { + TypeIndex uint16 + Size uint16 + IsResource bool +} + +var _ Instruction = InstructionNewArray{} + +func (InstructionNewArray) Opcode() Opcode { + return NewArray +} + +func (i InstructionNewArray) String() string { + var sb strings.Builder + sb.WriteString(i.Opcode().String()) + printfArgument(&sb, "typeIndex", i.TypeIndex) + printfArgument(&sb, "size", i.Size) + printfArgument(&sb, "isResource", i.IsResource) + return sb.String() +} + +func (i InstructionNewArray) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) + emitUint16(code, i.TypeIndex) + emitUint16(code, i.Size) + emitBool(code, i.IsResource) +} + +func DecodeNewArray(ip *uint16, code []byte) (i InstructionNewArray) { + i.TypeIndex = decodeUint16(ip, code) + i.Size = decodeUint16(ip, code) + i.IsResource = decodeBool(ip, code) + return i +} + +// InstructionNewRef +// +// Creates a new reference with the given type. +type InstructionNewRef struct { + TypeIndex uint16 +} + +var _ Instruction = InstructionNewRef{} + +func (InstructionNewRef) Opcode() Opcode { + return NewRef +} + +func (i InstructionNewRef) String() string { + var sb strings.Builder + sb.WriteString(i.Opcode().String()) + printfArgument(&sb, "typeIndex", i.TypeIndex) + return sb.String() +} + +func (i InstructionNewRef) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) + emitUint16(code, i.TypeIndex) +} + +func DecodeNewRef(ip *uint16, code []byte) (i InstructionNewRef) { + i.TypeIndex = decodeUint16(ip, code) + return i +} + +// InstructionGetConstant +// +// Pushes the constant at the given index onto the stack. +type InstructionGetConstant struct { + ConstantIndex uint16 +} + +var _ Instruction = InstructionGetConstant{} + +func (InstructionGetConstant) Opcode() Opcode { + return GetConstant +} + +func (i InstructionGetConstant) String() string { + var sb strings.Builder + sb.WriteString(i.Opcode().String()) + printfArgument(&sb, "constantIndex", i.ConstantIndex) + return sb.String() +} + +func (i InstructionGetConstant) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) + emitUint16(code, i.ConstantIndex) +} + +func DecodeGetConstant(ip *uint16, code []byte) (i InstructionGetConstant) { + i.ConstantIndex = decodeUint16(ip, code) + return i +} + +// InstructionInvoke +// +// Invokes the function with the given type arguments. +type InstructionInvoke struct { + TypeArgs []uint16 +} + +var _ Instruction = InstructionInvoke{} + +func (InstructionInvoke) Opcode() Opcode { + return Invoke +} + +func (i InstructionInvoke) String() string { + var sb strings.Builder + sb.WriteString(i.Opcode().String()) + printfUInt16ArrayArgument(&sb, "typeArgs", i.TypeArgs) + return sb.String() +} + +func (i InstructionInvoke) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) + emitUint16Array(code, i.TypeArgs) +} + +func DecodeInvoke(ip *uint16, code []byte) (i InstructionInvoke) { + i.TypeArgs = decodeUint16Array(ip, code) + return i +} + +// InstructionInvokeDynamic +// +// Invokes the dynamic function with the given name, type arguments, and argument count. +type InstructionInvokeDynamic struct { + NameIndex uint16 + TypeArgs []uint16 + ArgCount uint16 +} + +var _ Instruction = InstructionInvokeDynamic{} + +func (InstructionInvokeDynamic) Opcode() Opcode { + return InvokeDynamic +} + +func (i InstructionInvokeDynamic) String() string { + var sb strings.Builder + sb.WriteString(i.Opcode().String()) + printfArgument(&sb, "nameIndex", i.NameIndex) + printfUInt16ArrayArgument(&sb, "typeArgs", i.TypeArgs) + printfArgument(&sb, "argCount", i.ArgCount) + return sb.String() +} + +func (i InstructionInvokeDynamic) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) + emitUint16(code, i.NameIndex) + emitUint16Array(code, i.TypeArgs) + emitUint16(code, i.ArgCount) +} + +func DecodeInvokeDynamic(ip *uint16, code []byte) (i InstructionInvokeDynamic) { + i.NameIndex = decodeUint16(ip, code) + i.TypeArgs = decodeUint16Array(ip, code) + i.ArgCount = decodeUint16(ip, code) + return i +} + +// InstructionDup +// +// Duplicates the top value on the stack. +type InstructionDup struct { +} + +var _ Instruction = InstructionDup{} + +func (InstructionDup) Opcode() Opcode { + return Dup +} + +func (i InstructionDup) String() string { + return i.Opcode().String() +} + +func (i InstructionDup) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) +} + +// InstructionDrop +// +// Removes the top value from the stack. +type InstructionDrop struct { +} + +var _ Instruction = InstructionDrop{} + +func (InstructionDrop) Opcode() Opcode { + return Drop +} + +func (i InstructionDrop) String() string { + return i.Opcode().String() +} + +func (i InstructionDrop) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) +} + +// InstructionDestroy +// +// Destroys the top value on the stack. +type InstructionDestroy struct { +} + +var _ Instruction = InstructionDestroy{} + +func (InstructionDestroy) Opcode() Opcode { + return Destroy +} + +func (i InstructionDestroy) String() string { + return i.Opcode().String() +} + +func (i InstructionDestroy) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) +} + +// InstructionUnwrap +// +// Unwraps the top value on the stack. +type InstructionUnwrap struct { +} + +var _ Instruction = InstructionUnwrap{} + +func (InstructionUnwrap) Opcode() Opcode { + return Unwrap +} + +func (i InstructionUnwrap) String() string { + return i.Opcode().String() +} + +func (i InstructionUnwrap) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) +} + +// InstructionTransfer +// +// Transfers the top value on the stack. +type InstructionTransfer struct { + TypeIndex uint16 +} + +var _ Instruction = InstructionTransfer{} + +func (InstructionTransfer) Opcode() Opcode { + return Transfer +} + +func (i InstructionTransfer) String() string { + var sb strings.Builder + sb.WriteString(i.Opcode().String()) + printfArgument(&sb, "typeIndex", i.TypeIndex) + return sb.String() +} + +func (i InstructionTransfer) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) + emitUint16(code, i.TypeIndex) +} + +func DecodeTransfer(ip *uint16, code []byte) (i InstructionTransfer) { + i.TypeIndex = decodeUint16(ip, code) + return i +} + +// InstructionCast +// +// Casts the top value on the stack to the given type. +type InstructionCast struct { + TypeIndex uint16 + Kind CastKind +} + +var _ Instruction = InstructionCast{} + +func (InstructionCast) Opcode() Opcode { + return Cast +} + +func (i InstructionCast) 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) { + emitOpcode(code, i.Opcode()) + emitUint16(code, i.TypeIndex) + emitCastKind(code, i.Kind) +} + +func DecodeCast(ip *uint16, code []byte) (i InstructionCast) { + i.TypeIndex = decodeUint16(ip, code) + i.Kind = decodeCastKind(ip, code) + return i +} + +// InstructionJump +// +// Jumps to the given instruction. +type InstructionJump struct { + Target uint16 +} + +var _ Instruction = InstructionJump{} + +func (InstructionJump) Opcode() Opcode { + return Jump +} + +func (i InstructionJump) String() string { + var sb strings.Builder + sb.WriteString(i.Opcode().String()) + printfArgument(&sb, "target", i.Target) + return sb.String() +} + +func (i InstructionJump) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) + emitUint16(code, i.Target) +} + +func DecodeJump(ip *uint16, code []byte) (i InstructionJump) { + i.Target = decodeUint16(ip, code) + return i +} + +// InstructionJumpIfFalse +// +// Jumps to the given instruction, if the top value on the stack is `false`. +type InstructionJumpIfFalse struct { + Target uint16 +} + +var _ Instruction = InstructionJumpIfFalse{} + +func (InstructionJumpIfFalse) Opcode() Opcode { + return JumpIfFalse +} + +func (i InstructionJumpIfFalse) String() string { + var sb strings.Builder + sb.WriteString(i.Opcode().String()) + printfArgument(&sb, "target", i.Target) + return sb.String() +} + +func (i InstructionJumpIfFalse) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) + emitUint16(code, i.Target) +} + +func DecodeJumpIfFalse(ip *uint16, code []byte) (i InstructionJumpIfFalse) { + i.Target = decodeUint16(ip, code) + return i +} + +// InstructionReturn +// +// Returns from the current function, without a value. +type InstructionReturn struct { +} + +var _ Instruction = InstructionReturn{} + +func (InstructionReturn) Opcode() Opcode { + return Return +} + +func (i InstructionReturn) String() string { + return i.Opcode().String() +} + +func (i InstructionReturn) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) +} + +// InstructionReturnValue +// +// Returns from the current function, with the top value on the stack. +type InstructionReturnValue struct { +} + +var _ Instruction = InstructionReturnValue{} + +func (InstructionReturnValue) Opcode() Opcode { + return ReturnValue +} + +func (i InstructionReturnValue) String() string { + return i.Opcode().String() +} + +func (i InstructionReturnValue) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) +} + +// InstructionEqual +// +// Compares the top two values on the stack for equality. +type InstructionEqual struct { +} + +var _ Instruction = InstructionEqual{} + +func (InstructionEqual) Opcode() Opcode { + return Equal +} + +func (i InstructionEqual) String() string { + return i.Opcode().String() +} + +func (i InstructionEqual) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) +} + +// InstructionNotEqual +// +// Compares the top two values on the stack for inequality. +type InstructionNotEqual struct { +} + +var _ Instruction = InstructionNotEqual{} + +func (InstructionNotEqual) Opcode() Opcode { + return NotEqual +} + +func (i InstructionNotEqual) String() string { + return i.Opcode().String() +} + +func (i InstructionNotEqual) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) +} + +// InstructionIntAdd +// +// Adds the top two values on the stack. +type InstructionIntAdd struct { +} + +var _ Instruction = InstructionIntAdd{} + +func (InstructionIntAdd) Opcode() Opcode { + return IntAdd +} + +func (i InstructionIntAdd) String() string { + return i.Opcode().String() +} + +func (i InstructionIntAdd) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) +} + +// InstructionIntSubtract +// +// Subtracts the top two values on the stack. +type InstructionIntSubtract struct { +} + +var _ Instruction = InstructionIntSubtract{} + +func (InstructionIntSubtract) Opcode() Opcode { + return IntSubtract +} + +func (i InstructionIntSubtract) String() string { + return i.Opcode().String() +} + +func (i InstructionIntSubtract) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) +} + +// InstructionIntMultiply +// +// Multiplies the top two values on the stack. +type InstructionIntMultiply struct { +} + +var _ Instruction = InstructionIntMultiply{} + +func (InstructionIntMultiply) Opcode() Opcode { + return IntMultiply +} + +func (i InstructionIntMultiply) String() string { + return i.Opcode().String() +} + +func (i InstructionIntMultiply) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) +} + +// InstructionIntDivide +// +// Divides the top two values on the stack. +type InstructionIntDivide struct { +} + +var _ Instruction = InstructionIntDivide{} + +func (InstructionIntDivide) Opcode() Opcode { + return IntDivide +} + +func (i InstructionIntDivide) String() string { + return i.Opcode().String() +} + +func (i InstructionIntDivide) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) +} + +// InstructionIntMod +// +// Calculates the modulo of the top two values on the stack. +type InstructionIntMod struct { +} + +var _ Instruction = InstructionIntMod{} + +func (InstructionIntMod) Opcode() Opcode { + return IntMod +} + +func (i InstructionIntMod) String() string { + return i.Opcode().String() +} + +func (i InstructionIntMod) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) +} + +// InstructionIntLess +// +// Compares the top two values on the stack for less than. +type InstructionIntLess struct { +} + +var _ Instruction = InstructionIntLess{} + +func (InstructionIntLess) Opcode() Opcode { + return IntLess +} + +func (i InstructionIntLess) String() string { + return i.Opcode().String() +} + +func (i InstructionIntLess) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) +} + +// InstructionIntLessOrEqual +// +// Compares the top two values on the stack for less than or equal. +type InstructionIntLessOrEqual struct { +} + +var _ Instruction = InstructionIntLessOrEqual{} + +func (InstructionIntLessOrEqual) Opcode() Opcode { + return IntLessOrEqual +} + +func (i InstructionIntLessOrEqual) String() string { + return i.Opcode().String() +} + +func (i InstructionIntLessOrEqual) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) +} + +// InstructionIntGreater +// +// Compares the top two values on the stack for greater than. +type InstructionIntGreater struct { +} + +var _ Instruction = InstructionIntGreater{} + +func (InstructionIntGreater) Opcode() Opcode { + return IntGreater +} + +func (i InstructionIntGreater) String() string { + return i.Opcode().String() +} + +func (i InstructionIntGreater) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) +} + +// InstructionIntGreaterOrEqual +// +// Compares the top two values on the stack for greater than or equal. +type InstructionIntGreaterOrEqual struct { +} + +var _ Instruction = InstructionIntGreaterOrEqual{} + +func (InstructionIntGreaterOrEqual) Opcode() Opcode { + return IntGreaterOrEqual +} + +func (i InstructionIntGreaterOrEqual) String() string { + return i.Opcode().String() +} + +func (i InstructionIntGreaterOrEqual) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) +} + +func DecodeInstruction(ip *uint16, code []byte) Instruction { + switch Opcode(decodeByte(ip, code)) { + case Unknown: + return InstructionUnknown{} + case GetLocal: + return DecodeGetLocal(ip, code) + case SetLocal: + return DecodeSetLocal(ip, code) + case GetGlobal: + return DecodeGetGlobal(ip, code) + case SetGlobal: + return DecodeSetGlobal(ip, code) + case GetField: + return DecodeGetField(ip, code) + case SetField: + return DecodeSetField(ip, code) + case GetIndex: + return InstructionGetIndex{} + case SetIndex: + return InstructionSetIndex{} + case True: + return InstructionTrue{} + case False: + return InstructionFalse{} + case Nil: + return InstructionNil{} + case Path: + return DecodePath(ip, code) + case New: + return DecodeNew(ip, code) + case NewArray: + return DecodeNewArray(ip, code) + case NewRef: + return DecodeNewRef(ip, code) + case GetConstant: + return DecodeGetConstant(ip, code) + case Invoke: + return DecodeInvoke(ip, code) + case InvokeDynamic: + return DecodeInvokeDynamic(ip, code) + case Dup: + return InstructionDup{} + case Drop: + return InstructionDrop{} + case Destroy: + return InstructionDestroy{} + case Unwrap: + return InstructionUnwrap{} + case Transfer: + return DecodeTransfer(ip, code) + case Cast: + return DecodeCast(ip, code) + case Jump: + return DecodeJump(ip, code) + case JumpIfFalse: + return DecodeJumpIfFalse(ip, code) + case Return: + return InstructionReturn{} + case ReturnValue: + return InstructionReturnValue{} + case Equal: + return InstructionEqual{} + case NotEqual: + return InstructionNotEqual{} + case IntAdd: + return InstructionIntAdd{} + case IntSubtract: + return InstructionIntSubtract{} + case IntMultiply: + return InstructionIntMultiply{} + case IntDivide: + return InstructionIntDivide{} + case IntMod: + return InstructionIntMod{} + case IntLess: + return InstructionIntLess{} + case IntLessOrEqual: + return InstructionIntLessOrEqual{} + case IntGreater: + return InstructionIntGreater{} + case IntGreaterOrEqual: + return InstructionIntGreaterOrEqual{} + } + + panic(errors.NewUnreachableError()) +} diff --git a/bbq/opcode/instructions.yml b/bbq/opcode/instructions.yml new file mode 100644 index 0000000000..b9f466bfb3 --- /dev/null +++ b/bbq/opcode/instructions.yml @@ -0,0 +1,468 @@ + +- name: "unknown" + description: An unknown instruction. + +# Local instructions + +- name: "getLocal" + description: Pushes the value of the local at the given index onto the stack. + operands: + - name: "localIndex" + type: "index" + valueEffects: + push: + - name: "value" + type: "value" + +- name: "setLocal" + description: Sets the value of the local at the given index to the top value on the stack. + operands: + - name: "localIndex" + type: "index" + valueEffects: + pop: + - name: "value" + type: "value" + +# Global instructions + +- name: "getGlobal" + description: Pushes the value of the global at the given index onto the stack. + operands: + - name: "globalIndex" + type: "index" + valueEffects: + push: + - name: "value" + type: "value" + +- name: "setGlobal" + description: Sets the value of the global at the given index to the top value on the stack. + operands: + - name: "globalIndex" + type: "index" + valueEffects: + pop: + - name: "value" + type: "value" + +# Field instructions + +- name: "getField" + description: Pushes the value of the field at the given index onto the stack. + operands: + - name: "fieldNameIndex" + type: "index" + valueEffects: + pop: + - name: "container" + type: "value" + push: + - name: "value" + type: "value" + +- name: "setField" + description: Sets the value of the field at the given index to the top value on the stack. + operands: + - name: "fieldNameIndex" + type: "index" + valueEffects: + pop: + - name: "container" + type: "value" + - name: "value" + type: "value" + +# Index instructions + +- name: "getIndex" + description: Pushes the value at the given index onto the stack. + valueEffects: + pop: + - name: "array" + type: "array" + - name: "index" + type: "integer" + push: + - name: "value" + type: "value" + +- name: "setIndex" + description: Sets the value at the given index to the top value on the stack. + valueEffects: + pop: + - name: "array" + type: "array" + - name: "index" + type: "integer" + - name: "value" + type: "value" + +# Value instantiation instructions + +- name: "true" + description: Pushes the boolean value `true` onto the stack. + valueEffects: + push: + - name: "value" + type: "bool" + +- name: "false" + description: Pushes the boolean value `false` onto the stack. + valueEffects: + push: + - name: "value" + type: "bool" + +- name: "nil" + description: Pushes the value `nil` onto the stack. + valueEffects: + push: + - name: "value" + type: "value" + +- name: "path" + description: Pushes the path with the given domain and identifier onto the stack. + operands: + - name: "domain" + type: "pathDomain" + - name: "identifierIndex" + type: "index" + valueEffects: + push: + - name: "value" + type: "path" + +- name: "new" + description: Creates a new instance of the given type. + operands: + - name: "kind" + type: "compositeKind" + - name: "typeIndex" + type: "index" + valueEffects: + push: + - name: "value" + type: "value" + +- name: "newArray" + description: Creates a new array with the given type and size. + operands: + - name: "typeIndex" + type: "index" + - name: "size" + type: "size" + - name: "isResource" + type: "bool" + valueEffects: + pop: + - name: "elements" + type: "value" + # The number of elements taken from the stack is equal to the size operand of the opcode. + count: "size" + push: + - name: "array" + type: "array" + +- name: "newRef" + description: Creates a new reference with the given type. + operands: + - name: "typeIndex" + type: "index" + valueEffects: + pop: + - name: "value" + type: "value" + push: + - name: "reference" + type: "reference" + +- name: "getConstant" + description: Pushes the constant at the given index onto the stack. + operands: + - name: "constantIndex" + type: "index" + valueEffects: + push: + - name: "value" + type: "value" + +# Invocation instructions + +- name: "invoke" + description: Invokes the function with the given type arguments. + operands: + - name: "typeArgs" + type: "indices" + valueEffects: + pop: + - name: "arguments" + # TODO: count + - name: "function" + type: "function" + push: + - name: "result" + type: "value" + controlEffects: + - call: + +- name: "invokeDynamic" + description: Invokes the dynamic function with the given name, type arguments, and argument count. + operands: + - name: "nameIndex" + type: "index" + - name: "typeArgs" + type: "indices" + - name: "argCount" + type: "size" + valueEffects: + pop: + - name: "arguments" + # TODO: count + push: + - name: "result" + type: "value" + controlEffects: + - call: + +# Value stack instructions + +- name: "dup" + description: Duplicates the top value on the stack. + valueEffects: + pop: + - name: "value" + type: "value" + push: + - name: "original" + type: "value" + - name: "duplicate" + type: "value" + +- name: "drop" + description: Removes the top value from the stack. + valueEffects: + pop: + - name: "value" + type: "value" + +# Resource stack instructions + +- name: "destroy" + description: Destroys the top value on the stack. + valueEffects: + pop: + - name: "resource" + type: "resource" + +# Optional instructions + +- name: "unwrap" + description: Unwraps the top value on the stack. + valueEffects: + pop: + - name: "optional" + type: "optional" + push: + - name: "value" + type: "value" + +# Conversion instructions + +- name: "transfer" + description: Transfers the top value on the stack. + operands: + - name: "typeIndex" + type: "index" + valueEffects: + pop: + - name: "value" + type: "value" + push: + - name: "value" + type: "value" + +- name: "cast" + description: Casts the top value on the stack to the given type. + operands: + - name: "typeIndex" + type: "index" + - name: "kind" + type: "castKind" + valueEffects: + pop: + - name: "value" + type: "value" + push: + - name: "value" + type: "value" + +# Control flow instructions + +- name: "jump" + description: Jumps to the given instruction. + operands: + - name: "target" + type: "index" + controlEffects: + - jump: "target" + +- name: "jumpIfFalse" + description: Jumps to the given instruction, if the top value on the stack is `false`. + operands: + - name: "target" + type: "index" + controlEffects: + - jump: "target" + +- name: "return" + description: Returns from the current function, without a value. + controlEffects: + - return: + +- name: "returnValue" + description: Returns from the current function, with the top value on the stack. + valueEffects: + pop: + - name: "value" + type: "value" + controlEffects: + - return: "value" + +# Comparison instructions + +- name: "equal" + description: Compares the top two values on the stack for equality. + valueEffects: + pop: + - name: "left" + type: "value" + - name: "right" + type: "value" + push: + - name: "result" + type: "bool" + +- name: "notEqual" + description: Compares the top two values on the stack for inequality. + valueEffects: + pop: + - name: "left" + type: "value" + - name: "right" + type: "value" + push: + - name: "result" + type: "bool" + +# Integer arithmetic instructions + +- name: "intAdd" + description: Adds the top two values on the stack. + valueEffects: + pop: + - name: "left" + type: "int" + - name: "right" + type: "int" + push: + - name: "result" + type: "int" + +- name: "intSubtract" + description: Subtracts the top two values on the stack. + valueEffects: + pop: + - name: "left" + type: "int" + - name: "right" + type: "int" + push: + - name: "result" + type: "int" + +- name: "intMultiply" + description: Multiplies the top two values on the stack. + valueEffects: + pop: + - name: "left" + type: "int" + - name: "right" + type: "int" + push: + - name: "result" + type: "int" + +- name: "intDivide" + description: Divides the top two values on the stack. + valueEffects: + pop: + - name: "left" + type: "int" + - name: "right" + type: "int" + push: + - name: "result" + type: "int" + +- name: "intMod" + description: Calculates the modulo of the top two values on the stack. + valueEffects: + pop: + - name: "left" + type: "int" + - name: "right" + type: "int" + push: + - name: "result" + type: "int" + +# Integer comparison instructions + +- name: "intLess" + description: Compares the top two values on the stack for less than. + valueEffects: + pop: + - name: "left" + type: "int" + - name: "right" + type: "int" + push: + - name: "result" + type: "bool" + +- name: "intLessOrEqual" + description: Compares the top two values on the stack for less than or equal. + valueEffects: + pop: + - name: "left" + type: "int" + - name: "right" + type: "int" + push: + - name: "result" + type: "bool" + +- name: "intGreater" + description: Compares the top two values on the stack for greater than. + valueEffects: + pop: + - name: "left" + type: "int" + - name: "right" + type: "int" + push: + - name: "result" + type: "bool" + +- name: "intGreaterOrEqual" + description: Compares the top two values on the stack for greater than or equal. + valueEffects: + pop: + - name: "left" + type: "int" + - name: "right" + type: "int" + push: + - name: "result" + type: "bool" diff --git a/bbq/opcode/opcode.go b/bbq/opcode/opcode.go new file mode 100644 index 0000000000..98df78012a --- /dev/null +++ b/bbq/opcode/opcode.go @@ -0,0 +1,141 @@ +/* + * 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 + +//go:generate go run golang.org/x/tools/cmd/stringer -type=Opcode + +type Opcode byte + +const ( + Unknown Opcode = iota + + // Control flow + + Return + ReturnValue + Jump + JumpIfFalse + _ + _ + _ + _ + _ + _ + + // Int operations + + IntAdd + IntSubtract + IntMultiply + IntDivide + IntMod + IntLess + IntGreater + IntLessOrEqual + IntGreaterOrEqual + _ + _ + _ + _ + _ + _ + _ + _ + _ + _ + _ + + // Unary/Binary operators + + Equal + NotEqual + Unwrap + Destroy + Transfer + Cast + _ + _ + _ + _ + + // Value/Constant loading + + True + False + New + Path + Nil + NewArray + NewDictionary + NewRef + _ + _ + _ + _ + _ + _ + _ + _ + _ + _ + _ + _ + + GetConstant + GetLocal + SetLocal + GetGlobal + SetGlobal + GetField + SetField + SetIndex + GetIndex + _ + _ + _ + _ + _ + _ + _ + _ + _ + _ + _ + + // Invocations + + Invoke + InvokeDynamic + _ + _ + _ + _ + _ + _ + _ + _ + + // Stack operations + + Drop + Dup + _ + _ + _ +) diff --git a/bbq/opcode/opcode_string.go b/bbq/opcode/opcode_string.go new file mode 100644 index 0000000000..45b821b0c6 --- /dev/null +++ b/bbq/opcode/opcode_string.go @@ -0,0 +1,99 @@ +// Code generated by "stringer -type=Opcode"; DO NOT EDIT. + +package opcode + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[Unknown-0] + _ = x[Return-1] + _ = x[ReturnValue-2] + _ = x[Jump-3] + _ = x[JumpIfFalse-4] + _ = x[IntAdd-11] + _ = x[IntSubtract-12] + _ = x[IntMultiply-13] + _ = x[IntDivide-14] + _ = x[IntMod-15] + _ = x[IntLess-16] + _ = x[IntGreater-17] + _ = x[IntLessOrEqual-18] + _ = x[IntGreaterOrEqual-19] + _ = x[Equal-31] + _ = x[NotEqual-32] + _ = x[Unwrap-33] + _ = x[Destroy-34] + _ = x[Transfer-35] + _ = x[Cast-36] + _ = x[True-41] + _ = x[False-42] + _ = x[New-43] + _ = x[Path-44] + _ = x[Nil-45] + _ = x[NewArray-46] + _ = x[NewDictionary-47] + _ = x[NewRef-48] + _ = x[GetConstant-61] + _ = x[GetLocal-62] + _ = x[SetLocal-63] + _ = x[GetGlobal-64] + _ = x[SetGlobal-65] + _ = x[GetField-66] + _ = x[SetField-67] + _ = x[SetIndex-68] + _ = x[GetIndex-69] + _ = x[Invoke-81] + _ = x[InvokeDynamic-82] + _ = x[Drop-91] + _ = x[Dup-92] +} + +const ( + _Opcode_name_0 = "UnknownReturnReturnValueJumpJumpIfFalse" + _Opcode_name_1 = "IntAddIntSubtractIntMultiplyIntDivideIntModIntLessIntGreaterIntLessOrEqualIntGreaterOrEqual" + _Opcode_name_2 = "EqualNotEqualUnwrapDestroyTransferCast" + _Opcode_name_3 = "TrueFalseNewPathNilNewArrayNewDictionaryNewRef" + _Opcode_name_4 = "GetConstantGetLocalSetLocalGetGlobalSetGlobalGetFieldSetFieldSetIndexGetIndex" + _Opcode_name_5 = "InvokeInvokeDynamic" + _Opcode_name_6 = "DropDup" +) + +var ( + _Opcode_index_0 = [...]uint8{0, 7, 13, 24, 28, 39} + _Opcode_index_1 = [...]uint8{0, 6, 17, 28, 37, 43, 50, 60, 74, 91} + _Opcode_index_2 = [...]uint8{0, 5, 13, 19, 26, 34, 38} + _Opcode_index_3 = [...]uint8{0, 4, 9, 12, 16, 19, 27, 40, 46} + _Opcode_index_4 = [...]uint8{0, 11, 19, 27, 36, 45, 53, 61, 69, 77} + _Opcode_index_5 = [...]uint8{0, 6, 19} + _Opcode_index_6 = [...]uint8{0, 4, 7} +) + +func (i Opcode) String() string { + switch { + case i <= 4: + return _Opcode_name_0[_Opcode_index_0[i]:_Opcode_index_0[i+1]] + case 11 <= i && i <= 19: + i -= 11 + return _Opcode_name_1[_Opcode_index_1[i]:_Opcode_index_1[i+1]] + case 31 <= i && i <= 36: + i -= 31 + return _Opcode_name_2[_Opcode_index_2[i]:_Opcode_index_2[i+1]] + case 41 <= i && i <= 48: + i -= 41 + return _Opcode_name_3[_Opcode_index_3[i]:_Opcode_index_3[i+1]] + case 61 <= i && i <= 69: + i -= 61 + return _Opcode_name_4[_Opcode_index_4[i]:_Opcode_index_4[i+1]] + case 81 <= i && i <= 82: + i -= 81 + return _Opcode_name_5[_Opcode_index_5[i]:_Opcode_index_5[i+1]] + case 91 <= i && i <= 92: + i -= 91 + return _Opcode_name_6[_Opcode_index_6[i]:_Opcode_index_6[i+1]] + default: + return "Opcode(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/bbq/opcode/print.go b/bbq/opcode/print.go new file mode 100644 index 0000000000..5085c0bc41 --- /dev/null +++ b/bbq/opcode/print.go @@ -0,0 +1,40 @@ +/* + * 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 ( + "fmt" + "strings" +) + +func PrintBytecode(builder *strings.Builder, code []byte) error { + instructions := DecodeInstructions(code) + return PrintInstructions(builder, instructions) +} + +func PrintInstructions(builder *strings.Builder, instructions []Instruction) error { + for _, instruction := range instructions { + _, err := fmt.Fprint(builder, instruction) + if err != nil { + return err + } + builder.WriteByte('\n') + } + return nil +} diff --git a/bbq/opcode/print_test.go b/bbq/opcode/print_test.go new file mode 100644 index 0000000000..0f5f360d1a --- /dev/null +++ b/bbq/opcode/print_test.go @@ -0,0 +1,155 @@ +/* + * 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 ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPrintRecursionFib(t *testing.T) { + t.Parallel() + + code := []byte{ + // if n < 2 + byte(GetLocal), 0, 0, + byte(GetConstant), 0, 0, + byte(IntLess), + byte(JumpIfFalse), 0, 14, + // then return n + byte(GetLocal), 0, 0, + byte(ReturnValue), + // fib(n - 1) + byte(GetLocal), 0, 0, + byte(GetConstant), 0, 1, + byte(IntSubtract), + byte(Transfer), 0, 0, + byte(GetGlobal), 0, 0, + byte(Invoke), 0, 0, + // fib(n - 2) + byte(GetLocal), 0, 0, + byte(GetConstant), 0, 0, + byte(IntSubtract), + byte(Transfer), 0, 0, + byte(GetGlobal), 0, 0, + byte(Invoke), 0, 0, + // return sum + byte(IntAdd), + byte(ReturnValue), + } + + const expected = `GetLocal localIndex:0 +GetConstant constantIndex:0 +IntLess +JumpIfFalse target:14 +GetLocal localIndex:0 +ReturnValue +GetLocal localIndex:0 +GetConstant constantIndex:1 +IntSubtract +Transfer typeIndex:0 +GetGlobal globalIndex:0 +Invoke typeArgs:[] +GetLocal localIndex:0 +GetConstant constantIndex:0 +IntSubtract +Transfer typeIndex:0 +GetGlobal globalIndex:0 +Invoke typeArgs:[] +IntAdd +ReturnValue +` + + var builder strings.Builder + err := PrintBytecode(&builder, code) + require.NoError(t, err) + + assert.Equal(t, expected, builder.String()) +} + +func TestPrintInstruction(t *testing.T) { + t.Parallel() + + instructions := map[string][]byte{ + "GetConstant constantIndex:258": {byte(GetConstant), 1, 2}, + "GetLocal localIndex:258": {byte(GetLocal), 1, 2}, + "SetLocal localIndex:258": {byte(SetLocal), 1, 2}, + "GetGlobal globalIndex:258": {byte(GetGlobal), 1, 2}, + "SetGlobal globalIndex:258": {byte(SetGlobal), 1, 2}, + "Jump target:258": {byte(Jump), 1, 2}, + "JumpIfFalse target:258": {byte(JumpIfFalse), 1, 2}, + "Transfer typeIndex:258": {byte(Transfer), 1, 2}, + + "New kind:CompositeKind(258) typeIndex:772": {byte(New), 1, 2, 3, 4}, + + "Cast typeIndex:258 kind:3": {byte(Cast), 1, 2, 3}, + + `Path domain:PathDomainStorage identifierIndex:5`: {byte(Path), 1, 0, 5}, + + `InvokeDynamic nameIndex:1 typeArgs:[772, 1286] argCount:1800`: { + byte(InvokeDynamic), 0, 1, 0, 2, 3, 4, 5, 6, 7, 8, + }, + + "Invoke typeArgs:[772, 1286]": { + byte(Invoke), 0, 2, 3, 4, 5, 6, + }, + + "NewRef typeIndex:258": {byte(NewRef), 1, 2}, + + "NewArray typeIndex:258 size:772 isResource:true": {byte(NewArray), 1, 2, 3, 4, 1}, + + "Unknown": {byte(Unknown)}, + "Return": {byte(Return)}, + "ReturnValue": {byte(ReturnValue)}, + "IntAdd": {byte(IntAdd)}, + "IntSubtract": {byte(IntSubtract)}, + "IntMultiply": {byte(IntMultiply)}, + "IntDivide": {byte(IntDivide)}, + "IntMod": {byte(IntMod)}, + "IntLess": {byte(IntLess)}, + "IntGreater": {byte(IntGreater)}, + "IntLessOrEqual": {byte(IntLessOrEqual)}, + "IntGreaterOrEqual": {byte(IntGreaterOrEqual)}, + "Equal": {byte(Equal)}, + "NotEqual": {byte(NotEqual)}, + "Unwrap": {byte(Unwrap)}, + "Destroy": {byte(Destroy)}, + "True": {byte(True)}, + "False": {byte(False)}, + "Nil": {byte(Nil)}, + "GetField fieldNameIndex:258": {byte(GetField), 1, 2}, + "SetField fieldNameIndex:258": {byte(SetField), 1, 2}, + "SetIndex": {byte(SetIndex)}, + "GetIndex": {byte(GetIndex)}, + "Drop": {byte(Drop)}, + "Dup": {byte(Dup)}, + } + + for expected, code := range instructions { + t.Run(expected, func(t *testing.T) { + + var ip uint16 + instruction := DecodeInstruction(&ip, code) + assert.Equal(t, expected, instruction.String()) + }) + } +} diff --git a/bbq/program.go b/bbq/program.go new file mode 100644 index 0000000000..1dc1aa0c09 --- /dev/null +++ b/bbq/program.go @@ -0,0 +1,28 @@ +/* + * 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 bbq + +type Program[E any] struct { + Contract *Contract + Imports []*Import + Functions []*Function[E] + Constants []*Constant + Variables []*Variable + Types [][]byte +} diff --git a/bbq/program_printer.go b/bbq/program_printer.go new file mode 100644 index 0000000000..641e8a6e9c --- /dev/null +++ b/bbq/program_printer.go @@ -0,0 +1,138 @@ +/* + * 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 bbq + +import ( + "fmt" + "strings" + + "github.com/onflow/cadence/bbq/constantkind" + "github.com/onflow/cadence/bbq/leb128" + "github.com/onflow/cadence/bbq/opcode" + "github.com/onflow/cadence/errors" + "github.com/onflow/cadence/interpreter" +) + +type ProgramPrinter[E any] struct { + stringBuilder strings.Builder + codePrinter func(builder *strings.Builder, code []E) error +} + +func NewBytecodeProgramPrinter() *ProgramPrinter[byte] { + return &ProgramPrinter[byte]{ + codePrinter: opcode.PrintBytecode, + } +} + +func NewInstructionsProgramPrinter() *ProgramPrinter[opcode.Instruction] { + return &ProgramPrinter[opcode.Instruction]{ + codePrinter: opcode.PrintInstructions, + } +} + +func (p *ProgramPrinter[E]) PrintProgram(program *Program[E]) string { + p.printImports(program.Imports) + p.printConstantPool(program.Constants) + p.printTypePool(program.Types) + + for _, function := range program.Functions { + p.printFunction(function) + p.stringBuilder.WriteRune('\n') + } + + return p.stringBuilder.String() +} + +func (p *ProgramPrinter[E]) printFunction(function *Function[E]) { + p.stringBuilder.WriteString("-- " + function.Name + " --\n") + err := p.codePrinter(&p.stringBuilder, function.Code) + if err != nil { + // TODO: propagate error + panic(err) + } +} + +func (*ProgramPrinter[_]) getIntOperand(codes []byte, i int) (operand int, endIndex int) { + first := codes[i+1] + last := codes[i+2] + operand = int(uint16(first)<<8 | uint16(last)) + return operand, i + 2 +} + +func (p *ProgramPrinter[_]) printConstantPool(constants []*Constant) { + p.stringBuilder.WriteString("-- Constant Pool --\n") + + for index, constant := range constants { + var constantStr string + + // TODO: duplicate of `VM.initializeConstant()` + switch constant.Kind { + case constantkind.Int: + smallInt, _, _ := leb128.ReadInt64(constant.Data) + constantStr = fmt.Sprint(smallInt) + case constantkind.String: + constantStr = string(constant.Data) + default: + panic(errors.NewUnreachableError()) + } + + p.stringBuilder.WriteString(fmt.Sprint(index)) + p.stringBuilder.WriteString(" | ") + p.stringBuilder.WriteString(constant.Kind.String()) + p.stringBuilder.WriteString(" | ") + p.stringBuilder.WriteString(constantStr) + p.stringBuilder.WriteRune('\n') + } + + p.stringBuilder.WriteRune('\n') +} + +func (p *ProgramPrinter[_]) printTypePool(types [][]byte) { + p.stringBuilder.WriteString("-- Type Pool --\n") + + for index, typeBytes := range types { + dec := interpreter.CBORDecMode.NewByteStreamDecoder(typeBytes) + typeDecoder := interpreter.NewTypeDecoder(dec, nil) + staticType, err := typeDecoder.DecodeStaticType() + if err != nil { + panic(err) + } + + p.stringBuilder.WriteString(fmt.Sprint(index)) + p.stringBuilder.WriteString(" | ") + p.stringBuilder.WriteString(string(staticType.ID())) + p.stringBuilder.WriteRune('\n') + } + + p.stringBuilder.WriteRune('\n') +} + +func (p *ProgramPrinter[_]) printImports(imports []*Import) { + p.stringBuilder.WriteString("-- Imports --\n") + for _, impt := range imports { + location := impt.Location + if location != nil { + p.stringBuilder.WriteString(location.String()) + p.stringBuilder.WriteRune('.') + } + p.stringBuilder.WriteString(impt.Name) + p.stringBuilder.WriteRune('\n') + } + p.stringBuilder.WriteRune('\n') +} diff --git a/bbq/variable.go b/bbq/variable.go new file mode 100644 index 0000000000..521e22a1b1 --- /dev/null +++ b/bbq/variable.go @@ -0,0 +1,23 @@ +/* + * 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 bbq + +type Variable struct { + Name string +} diff --git a/bbq/vm/callframe.go b/bbq/vm/callframe.go new file mode 100644 index 0000000000..25c57d6dc1 --- /dev/null +++ b/bbq/vm/callframe.go @@ -0,0 +1,31 @@ +/* + * 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" + "github.com/onflow/cadence/bbq/opcode" +) + +type callFrame struct { + executable *ExecutableProgram + localsOffset uint16 + localsCount uint16 + function *bbq.Function[opcode.Instruction] +} diff --git a/bbq/vm/config.go b/bbq/vm/config.go new file mode 100644 index 0000000000..8f8876d48d --- /dev/null +++ b/bbq/vm/config.go @@ -0,0 +1,99 @@ +/* + * 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/common" + "github.com/onflow/cadence/interpreter" + "github.com/onflow/cadence/sema" + "github.com/onflow/cadence/stdlib" + "github.com/onflow/cadence/test_utils/common_utils" +) + +type Config struct { + interpreter.Storage + common.MemoryGauge + commons.ImportHandler + ContractValueHandler + stdlib.AccountHandler + + // TODO: Move these to a 'shared state'? + CapabilityControllerIterations map[AddressPath]int + MutationDuringCapabilityControllerIteration bool + referencedResourceKindedValues ReferencedResourceKindedValues + + // TODO: These are temporary. Remove once storing/reading is supported for VM values. + inter *interpreter.Interpreter + TypeLoader func(location common.Location, typeID interpreter.TypeID) sema.CompositeKindedType +} + +func NewConfig(storage interpreter.Storage) *Config { + return &Config{ + Storage: storage, + MemoryGauge: nil, + ImportHandler: nil, + ContractValueHandler: nil, + AccountHandler: nil, + + CapabilityControllerIterations: make(map[AddressPath]int), + MutationDuringCapabilityControllerIteration: false, + referencedResourceKindedValues: ReferencedResourceKindedValues{}, + } +} + +func (c *Config) WithAccountHandler(handler stdlib.AccountHandler) *Config { + c.AccountHandler = handler + return c +} + +// TODO: This is temporary. Remove once storing/reading is supported for VM values. +func (c *Config) interpreter() *interpreter.Interpreter { + if c.inter == nil { + inter, err := interpreter.NewInterpreter( + nil, + common_utils.TestLocation, + &interpreter.Config{ + Storage: c.Storage, + ImportLocationHandler: nil, + CompositeTypeHandler: func(location common.Location, typeID interpreter.TypeID) *sema.CompositeType { + return c.TypeLoader(location, typeID).(*sema.CompositeType) + }, + InterfaceTypeHandler: func(location common.Location, typeID interpreter.TypeID) *sema.InterfaceType { + return c.TypeLoader(location, typeID).(*sema.InterfaceType) + }, + }, + ) + + if err != nil { + panic(err) + } + + c.inter = inter + } + + return c.inter +} + +type ContractValueHandler func(conf *Config, location common.Location) *CompositeValue + +type AddressPath struct { + Address common.Address + Path PathValue +} diff --git a/bbq/vm/errors.go b/bbq/vm/errors.go new file mode 100644 index 0000000000..661aed9b30 --- /dev/null +++ b/bbq/vm/errors.go @@ -0,0 +1,54 @@ +/* + * 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 ( + "fmt" + "github.com/onflow/cadence/errors" +) + +type LinkerError struct { + Message string +} + +var _ error = LinkerError{} +var _ errors.InternalError = LinkerError{} + +func (l LinkerError) IsInternalError() { +} + +func (l LinkerError) Error() string { + return l.Message +} + +type MissingMemberValueError struct { + Parent MemberAccessibleValue + Name string +} + +var _ error = MissingMemberValueError{} +var _ errors.InternalError = MissingMemberValueError{} + +func (l MissingMemberValueError) IsInternalError() { +} + +func (l MissingMemberValueError) Error() string { + return fmt.Sprintf("cannot find member: `%s` in `%T`", l.Name, l.Parent) + +} diff --git a/bbq/vm/executable_program.go b/bbq/vm/executable_program.go new file mode 100644 index 0000000000..f01ed2940c --- /dev/null +++ b/bbq/vm/executable_program.go @@ -0,0 +1,69 @@ +/* + * 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" + "github.com/onflow/cadence/bbq/opcode" + "github.com/onflow/cadence/common" +) + +// ExecutableProgram is the 'executable' version of a `bbq.Program`. +// It holds information that are accessible to a given program, +// such as constants, static-types, and global variables. +// These info are accessed by the opcodes of the program. +// i.e: indexes used in opcodes refer to the indexes of its ExecutableProgram. +type ExecutableProgram struct { + Location common.Location + Program *bbq.Program[opcode.Instruction] + Globals []Value + Constants []Value + StaticTypes []StaticType +} + +func NewExecutableProgram( + location common.Location, + program *bbq.Program[opcode.Instruction], + globals []Value, +) *ExecutableProgram { + return &ExecutableProgram{ + Location: location, + Program: program, + Globals: globals, + Constants: make([]Value, len(program.Constants)), + StaticTypes: make([]StaticType, len(program.Types)), + } +} + +// NewLoadedExecutableProgram returns an ExecutableProgram with types decoded. +// Note that the returned program **doesn't** have the globals linked. +func NewLoadedExecutableProgram( + location common.Location, + program *bbq.Program[opcode.Instruction], +) *ExecutableProgram { + executable := NewExecutableProgram(location, program, nil) + + // Optimization: Pre load/decode types + for index, bytes := range program.Types { + staticType := decodeType(bytes) + executable.StaticTypes[index] = staticType + } + + return executable +} diff --git a/bbq/vm/linker.go b/bbq/vm/linker.go new file mode 100644 index 0000000000..b218d95549 --- /dev/null +++ b/bbq/vm/linker.go @@ -0,0 +1,140 @@ +/* + * 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 ( + "fmt" + + "github.com/onflow/cadence/bbq" + "github.com/onflow/cadence/bbq/opcode" + + "github.com/onflow/cadence/common" +) + +type LinkedGlobals struct { + // context shared by the globals in the program. + executable *ExecutableProgram + + // globals defined in the program, indexed by name. + indexedGlobals map[string]Value +} + +// LinkGlobals performs the linking of global functions and variables for a given program. +func LinkGlobals( + location common.Location, + program *bbq.Program[opcode.Instruction], + conf *Config, + linkedGlobalsCache map[common.Location]LinkedGlobals, +) LinkedGlobals { + + var importedGlobals []Value + + for _, programImport := range program.Imports { + importLocation := programImport.Location + linkedGlobals, ok := linkedGlobalsCache[importLocation] + + if !ok { + importedProgram := conf.ImportHandler(importLocation) + + // Link and get all globals at the import location. + linkedGlobals = LinkGlobals( + importLocation, + importedProgram, + conf, + linkedGlobalsCache, + ) + + // If the imported program is a contract, + // load the contract value and populate the global variable. + if importedProgram.Contract != nil { + contract := importedProgram.Contract + location := common.NewAddressLocation( + conf.MemoryGauge, + common.MustBytesToAddress(contract.Address), + contract.Name, + ) + + // TODO: remove this check. This shouldn't be nil ideally. + if conf.ContractValueHandler != nil { + contractValue := conf.ContractValueHandler(conf, location) + // Update the globals - both the context and the mapping. + // Contract value is always at the zero-th index. + linkedGlobals.executable.Globals[0] = contractValue + linkedGlobals.indexedGlobals[contract.Name] = contractValue + } + } + + linkedGlobalsCache[importLocation] = linkedGlobals + } + + importedGlobal, ok := linkedGlobals.indexedGlobals[programImport.Name] + if !ok { + panic(LinkerError{ + Message: fmt.Sprintf("cannot find import '%s'", programImport.Name), + }) + } + importedGlobals = append(importedGlobals, importedGlobal) + } + + executable := NewLoadedExecutableProgram(location, program) + + globalsLen := len(program.Variables) + len(program.Functions) + len(importedGlobals) + 1 + indexedGlobalsLen := len(program.Functions) + + globals := make([]Value, 0, globalsLen) + indexedGlobals := make(map[string]Value, indexedGlobalsLen) + + // If the current program is a contract, reserve a global variable for the contract value. + // The reserved position is always the zero-th index. + // This value will be populated either by the `init` method invocation of the contract, + // Or when this program is imported by another (loads the value from storage). + if program.Contract != nil { + globals = append(globals, nil) + } + + for range program.Variables { + globals = append(globals, nil) + } + + // Iterate through `program.Functions` to be deterministic. + // Order of globals must be same as index set at `Compiler.addGlobal()`. + // TODO: include non-function globals + for _, function := range program.Functions { + value := FunctionValue{ + Function: function, + Executable: executable, + } + + globals = append(globals, value) + indexedGlobals[function.Name] = value + } + + // Globals of the current program are added first. + // This is the same order as they are added in the compiler. + // e.g: [global1, global2, ... [importedGlobal1, importedGlobal2, ...]] + executable.Globals = globals + executable.Globals = append(executable.Globals, importedGlobals...) + + // Return only the globals defined in the current program. + // Because the importer/caller doesn't need to know globals of nested imports. + return LinkedGlobals{ + executable: executable, + indexedGlobals: indexedGlobals, + } +} diff --git a/bbq/vm/native_functions.go b/bbq/vm/native_functions.go new file mode 100644 index 0000000000..7a85a3ee8d --- /dev/null +++ b/bbq/vm/native_functions.go @@ -0,0 +1,87 @@ +/* + * 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 ( + "fmt" + + "github.com/onflow/cadence/bbq/commons" + + "github.com/onflow/cadence/common" + "github.com/onflow/cadence/errors" + "github.com/onflow/cadence/stdlib" +) + +var nativeFunctions = map[string]Value{} + +// BuiltInLocation is the location of built-in constructs. +// It's always nil. +var BuiltInLocation common.Location = nil + +func NativeFunctions() map[string]Value { + funcs := make(map[string]Value, len(nativeFunctions)) + for name, value := range nativeFunctions { + funcs[name] = value + } + return funcs +} + +func RegisterFunction(functionName string, functionValue NativeFunctionValue) { + functionValue.Name = functionName + nativeFunctions[functionName] = functionValue +} + +func RegisterTypeBoundFunction(typeName, functionName string, functionValue NativeFunctionValue) { + // +1 is for the receiver + functionValue.ParameterCount++ + qualifiedName := commons.TypeQualifiedName(typeName, functionName) + RegisterFunction(qualifiedName, functionValue) +} + +func init() { + RegisterFunction(commons.LogFunctionName, NativeFunctionValue{ + ParameterCount: len(stdlib.LogFunctionType.Parameters), + Function: func(config *Config, typeArguments []StaticType, arguments ...Value) Value { + // TODO: Properly implement + fmt.Println(arguments[0].String()) + return VoidValue{} + }, + }) + + RegisterFunction(commons.PanicFunctionName, NativeFunctionValue{ + ParameterCount: len(stdlib.PanicFunctionType.Parameters), + Function: func(config *Config, typeArguments []StaticType, arguments ...Value) Value { + // TODO: Properly implement + messageValue, ok := arguments[0].(StringValue) + if !ok { + panic(errors.NewUnreachableError()) + } + + panic(string(messageValue.Str)) + }, + }) + + RegisterFunction(commons.GetAccountFunctionName, NativeFunctionValue{ + ParameterCount: len(stdlib.PanicFunctionType.Parameters), + Function: func(config *Config, typeArguments []StaticType, arguments ...Value) Value { + address := arguments[0].(AddressValue) + return NewAccountReferenceValue(config, common.Address(address)) + }, + }) +} diff --git a/bbq/vm/reference_tracking.go b/bbq/vm/reference_tracking.go new file mode 100644 index 0000000000..e468a761d0 --- /dev/null +++ b/bbq/vm/reference_tracking.go @@ -0,0 +1,161 @@ +/* + * 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/atree" + + "github.com/onflow/cadence/interpreter" +) + +type ReferencedResourceKindedValues map[atree.ValueID]map[*EphemeralReferenceValue]struct{} + +func maybeTrackReferencedResourceKindedValue(conf *Config, value Value) { + referenceValue, ok := value.(*EphemeralReferenceValue) + if !ok { + return + } + + referenceTRackedValue, ok := referenceValue.Value.(ReferenceTrackedResourceKindedValue) + if !ok { + return + } + + trackReferencedResourceKindedValue( + conf, + referenceTRackedValue.ValueID(), + referenceValue, + ) +} + +func trackReferencedResourceKindedValue( + conf *Config, + id atree.ValueID, + value *EphemeralReferenceValue, +) { + values := conf.referencedResourceKindedValues[id] + if values == nil { + values = map[*EphemeralReferenceValue]struct{}{} + conf.referencedResourceKindedValues[id] = values + } + values[value] = struct{}{} +} + +func checkInvalidatedResourceOrResourceReference(value Value) { + + switch value := value.(type) { + case *SomeValue: + checkInvalidatedResourceOrResourceReference(value.value) + + // TODO: + //case ResourceKindedValue: + // if value.isInvalidatedResource(interpreter) { + // panic(InvalidatedResourceError{ + // LocationRange: LocationRange{ + // Location: interpreter.Location, + // HasPosition: hasPosition, + // }, + // }) + // } + case *EphemeralReferenceValue: + if value.Value == nil { + panic(interpreter.InvalidatedResourceReferenceError{ + //LocationRange: interpreter.LocationRange{ + // Location: interpreter.Location, + // HasPosition: hasPosition, + //}, + }) + } else { + // If the value is there, check whether the referenced value is an invalidated one. + // This step is not really needed, since reference tracking is supposed to clear the + // `value.Value` if the referenced-value was moved/deleted. + // However, have this as a second layer of defensive. + checkInvalidatedResourceOrResourceReference(value.Value) + } + } +} + +func invalidateReferencedResources( + conf *Config, + value Value, +) { + // skip non-resource typed values + resourceKinded, ok := value.(ResourceKindedValue) + if !ok || !resourceKinded.IsResourceKinded() { + return + } + + var valueID atree.ValueID + + switch value := resourceKinded.(type) { + case *CompositeValue: + value.ForEachReadOnlyLoadedField( + conf, + func(_ string, fieldValue Value) (resume bool) { + invalidateReferencedResources(conf, fieldValue) + // continue iteration + return true + }, + ) + valueID = value.ValueID() + + case *DictionaryValue: + value.IterateReadOnlyLoaded( + conf, + func(_, value Value) (resume bool) { + invalidateReferencedResources(conf, value) + return true + }, + ) + valueID = value.ValueID() + + case *ArrayValue: + value.IterateReadOnlyLoaded( + conf, + func(element Value) (resume bool) { + invalidateReferencedResources(conf, element) + return true + }, + ) + valueID = value.ValueID() + + case *SomeValue: + invalidateReferencedResources(conf, value.value) + return + + default: + // skip non-container typed values. + return + } + + values := conf.referencedResourceKindedValues[valueID] + if values == nil { + return + } + + for value := range values { //nolint:maprange + value.Value = nil + } + + // The old resource instances are already cleared/invalidated above. + // So no need to track those stale resources anymore. We will not need to update/clear them again. + // Therefore, remove them from the mapping. + // This is only to allow GC. No impact to the behavior. + delete(conf.referencedResourceKindedValues, valueID) +} diff --git a/bbq/vm/storage.go b/bbq/vm/storage.go new file mode 100644 index 0000000000..da0171e9d2 --- /dev/null +++ b/bbq/vm/storage.go @@ -0,0 +1,157 @@ +/* + * 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/atree" + "github.com/onflow/cadence/errors" + + "github.com/onflow/cadence/common" + "github.com/onflow/cadence/interpreter" +) + +// type Storage interface { +// atree.SlabStorage +// GetStorageMap(address common.Address, domain string, createIfNotExists bool) *StorageMap +// CheckHealth() error +// } + +func StoredValue(gauge common.MemoryGauge, storable atree.Storable, storage interpreter.Storage) Value { + value := interpreter.StoredValue(gauge, storable, storage) + return InterpreterValueToVMValue(storage, value) +} + +func MustConvertStoredValue(gauge common.MemoryGauge, storage interpreter.Storage, storedValue atree.Value) Value { + value := interpreter.MustConvertStoredValue(gauge, storedValue) + return InterpreterValueToVMValue(storage, value) +} + +func ReadStored( + gauge common.MemoryGauge, + storage interpreter.Storage, + address common.Address, + domain string, + identifier string, +) Value { + accountStorage := storage.GetStorageMap(address, domain, false) + if accountStorage == nil { + return nil + } + + referenced := accountStorage.ReadValue(gauge, interpreter.StringStorageMapKey(identifier)) + return InterpreterValueToVMValue(storage, referenced) +} + +func WriteStored( + config *Config, + storageAddress common.Address, + domain string, + key interpreter.StorageMapKey, + value Value, +) (existed bool) { + accountStorage := config.Storage.GetStorageMap(storageAddress, domain, true) + interValue := VMValueToInterpreterValue(config, value) + + return accountStorage.WriteValue( + config.interpreter(), + key, + interValue, + ) + //interpreter.recordStorageMutation() +} + +func RemoveReferencedSlab(storage interpreter.Storage, storable atree.Storable) { + slabIDStorable, ok := storable.(atree.SlabIDStorable) + if !ok { + return + } + + slabID := atree.SlabID(slabIDStorable) + err := storage.Remove(slabID) + if err != nil { + panic(errors.NewExternalError(err)) + } +} + +func StoredValueExists( + storage interpreter.Storage, + storageAddress common.Address, + domain string, + identifier interpreter.StorageMapKey, +) bool { + accountStorage := storage.GetStorageMap(storageAddress, domain, false) + if accountStorage == nil { + return false + } + return accountStorage.ValueExists(identifier) +} + +// +//// InMemoryStorage +//type InMemoryStorage struct { +// *atree.BasicSlabStorage +// StorageMaps map[interpreter.StorageKey]*StorageMap +// memoryGauge common.MemoryGauge +//} +// +//var _ Storage = InMemoryStorage{} +// +//func NewInMemoryStorage(memoryGauge common.MemoryGauge) InMemoryStorage { +// decodeStorable := func(decoder *cbor.StreamDecoder, storableSlabStorageID atree.StorageID) (atree.Storable, error) { +// return interpreter.DecodeStorable(decoder, storableSlabStorageID, memoryGauge) +// } +// +// decodeTypeInfo := func(decoder *cbor.StreamDecoder) (atree.TypeInfo, error) { +// return interpreter.DecodeTypeInfo(decoder, memoryGauge) +// } +// +// slabStorage := atree.NewBasicSlabStorage( +// interpreter.CBOREncMode, +// interpreter.CBORDecMode, +// decodeStorable, +// decodeTypeInfo, +// ) +// +// return InMemoryStorage{ +// BasicSlabStorage: slabStorage, +// StorageMaps: make(map[interpreter.StorageKey]*StorageMap), +// memoryGauge: memoryGauge, +// } +//} +// +//func (i InMemoryStorage) GetStorageMap( +// address common.Address, +// domain string, +// createIfNotExists bool, +//) ( +// storageMap *StorageMap, +//) { +// key := interpreter.NewStorageKey(i.memoryGauge, address, domain) +// storageMap = i.StorageMaps[key] +// if storageMap == nil && createIfNotExists { +// storageMap = NewStorageMap(i.memoryGauge, i, atree.Address(address)) +// i.StorageMaps[key] = storageMap +// } +// return storageMap +//} +// +//func (i InMemoryStorage) CheckHealth() error { +// _, err := atree.CheckStorageHealth(i, -1) +// return err +//} diff --git a/bbq/vm/storage_map.go b/bbq/vm/storage_map.go new file mode 100644 index 0000000000..2c25cd97d9 --- /dev/null +++ b/bbq/vm/storage_map.go @@ -0,0 +1,240 @@ +/* + * 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 + +//// StorageMap is an ordered map which stores values in an account. +//type StorageMap struct { +// orderedMap *atree.OrderedMap +//} +// +//func NewStorageMap(memoryGauge common.MemoryGauge, storage atree.SlabStorage, address atree.Address) *StorageMap { +// common.UseMemory(memoryGauge, common.StorageMapMemoryUsage) +// +// orderedMap, err := atree.NewMap( +// storage, +// address, +// atree.NewDefaultDigesterBuilder(), +// interpreter.EmptyTypeInfo{}, +// ) +// if err != nil { +// panic(errors.NewExternalError(err)) +// } +// +// return &StorageMap{ +// orderedMap: orderedMap, +// } +//} +// +//func NewStorageMapWithRootID(storage atree.SlabStorage, storageID atree.StorageID) *StorageMap { +// orderedMap, err := atree.NewMapWithRootID( +// storage, +// storageID, +// atree.NewDefaultDigesterBuilder(), +// ) +// if err != nil { +// panic(errors.NewExternalError(err)) +// } +// +// return &StorageMap{ +// orderedMap: orderedMap, +// } +//} +// +//// ValueExists returns true if the given key exists in the storage map. +//func (s StorageMap) ValueExists(key string) bool { +// _, err := s.orderedMap.Get( +// interpreter.StringAtreeComparator, +// interpreter.StringAtreeHashInput, +// interpreter.StringAtreeValue(key), +// ) +// if err != nil { +// if _, ok := err.(*atree.KeyNotFoundError); ok { +// return false +// } +// panic(errors.NewExternalError(err)) +// } +// +// return true +//} +// +//// ReadValue returns the value for the given key. +//// Returns nil if the key does not exist. +//func (s StorageMap) ReadValue(gauge common.MemoryGauge, key string) Value { +// storable, err := s.orderedMap.Get( +// interpreter.StringAtreeComparator, +// interpreter.StringAtreeHashInput, +// interpreter.StringAtreeValue(key), +// ) +// if err != nil { +// if _, ok := err.(*atree.KeyNotFoundError); ok { +// return nil +// } +// panic(errors.NewExternalError(err)) +// } +// +// return StoredValue(gauge, storable, s.orderedMap.Storage) +//} +// +//// WriteValue sets or removes a value in the storage map. +//// If the given value is nil, the key is removed. +//// If the given value is non-nil, the key is added/updated. +//func (s StorageMap) WriteValue(gauge common.MemoryGauge, key string, value atree.Value) { +// if value == nil { +// s.RemoveValue(gauge, key) +// } else { +// s.SetValue(gauge, key, value) +// } +//} +// +//// SetValue sets a value in the storage map. +//// If the given key already stores a value, it is overwritten. +//func (s StorageMap) SetValue(gauge common.MemoryGauge, key string, value atree.Value) { +// existingStorable, err := s.orderedMap.Set( +// interpreter.StringAtreeComparator, +// interpreter.StringAtreeHashInput, +// interpreter.StringAtreeValue(key), +// value, +// ) +// if err != nil { +// panic(errors.NewExternalError(err)) +// } +// +// // TODO: +// //interpreter.maybeValidateAtreeValue(s.orderedMap) +// +// if existingStorable != nil { +// existingValue := interpreter.StoredValue(gauge, existingStorable, s.orderedMap.Storage) +// existingValue.DeepRemove(inter) +// //RemoveReferencedSlab(s.orderedMap.Storage, existingStorable) +// } +//} +// +//// RemoveValue removes a value in the storage map, if it exists. +//func (s StorageMap) RemoveValue(gauge common.MemoryGauge, key string) { +// _ /*existingKeyStorable*/, existingValueStorable, err := s.orderedMap.Remove( +// interpreter.StringAtreeComparator, +// interpreter.StringAtreeHashInput, +// interpreter.StringAtreeValue(key), +// ) +// if err != nil { +// if _, ok := err.(*atree.KeyNotFoundError); ok { +// return +// } +// panic(errors.NewExternalError(err)) +// } +// +// // TODO +// //interpreter.maybeValidateAtreeValue(s.orderedMap) +// +// // Key +// +// // NOTE: key / field name is stringAtreeValue, +// // and not a Value, so no need to deep remove +// // TODO +// //interpreter.RemoveReferencedSlab(existingKeyStorable) +// +// // Value +// +// if existingValueStorable != nil { +// _ = StoredValue(gauge, existingValueStorable, s.orderedMap.Storage) +// //existingValue.DeepRemove(interpreter) +// //interpreter.RemoveReferencedSlab(existingValueStorable) +// } +//} +// +//// Iterator returns an iterator (StorageMapIterator), +//// which allows iterating over the keys and values of the storage map +//func (s StorageMap) Iterator(gauge common.MemoryGauge) StorageMapIterator { +// mapIterator, err := s.orderedMap.Iterator() +// if err != nil { +// panic(errors.NewExternalError(err)) +// } +// +// return StorageMapIterator{ +// gauge: gauge, +// mapIterator: mapIterator, +// storage: s.orderedMap.Storage, +// } +//} +// +//func (s StorageMap) StorageID() atree.StorageID { +// return s.orderedMap.StorageID() +//} +// +//func (s StorageMap) Count() uint64 { +// return s.orderedMap.Count() +//} +// +//// StorageMapIterator is an iterator over StorageMap +//type StorageMapIterator struct { +// gauge common.MemoryGauge +// mapIterator *atree.MapIterator +// storage atree.SlabStorage +//} +// +//// Next returns the next key and value of the storage map iterator. +//// If there is no further key-value pair, ("", nil) is returned. +//func (i StorageMapIterator) Next() (string, Value) { +// k, v, err := i.mapIterator.Next() +// if err != nil { +// panic(errors.NewExternalError(err)) +// } +// +// if k == nil || v == nil { +// return "", nil +// } +// +// key := string(k.(interpreter.StringAtreeValue)) +// value := interpreter.MustConvertStoredValue(i.gauge, v) +// +// vmValue := InterpreterValueToVMValue(i.storage, value) +// return key, vmValue +//} +// +//// NextKey returns the next key of the storage map iterator. +//// If there is no further key, "" is returned. +//func (i StorageMapIterator) NextKey() string { +// k, err := i.mapIterator.NextKey() +// if err != nil { +// panic(errors.NewExternalError(err)) +// } +// +// if k == nil { +// return "" +// } +// +// return string(k.(interpreter.StringAtreeValue)) +//} +// +//// NextValue returns the next value in the storage map iterator. +//// If there is nop further value, nil is returned. +//func (i StorageMapIterator) NextValue() Value { +// v, err := i.mapIterator.NextValue() +// if err != nil { +// panic(errors.NewExternalError(err)) +// } +// +// if v == nil { +// return nil +// } +// +// value := interpreter.MustConvertStoredValue(i.gauge, v) +// +// return InterpreterValueToVMValue(i.storage, value) +//} diff --git a/bbq/vm/test/ft_contracts_and_txs.go b/bbq/vm/test/ft_contracts_and_txs.go new file mode 100644 index 0000000000..bcd8b51fb8 --- /dev/null +++ b/bbq/vm/test/ft_contracts_and_txs.go @@ -0,0 +1,459 @@ +/* + * 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 test + +const realFungibleTokenContractInterface = ` +/// FungibleToken +/// +/// The interface that fungible token contracts implement. +/// +access(all) contract interface FungibleToken { + + /// The total number of tokens in existence. + /// It is up to the implementer to ensure that the total supply + /// stays accurate and up to date + /// + access(all) var totalSupply: Int + + /// TokensInitialized + /// + /// The event that is emitted when the contract is created + /// + access(all) event TokensInitialized(initialSupply: Int) + + /// TokensWithdrawn + /// + /// The event that is emitted when tokens are withdrawn from a Vault + /// + access(all) event TokensWithdrawn(amount: Int, from: Address?) + + /// TokensDeposited + /// + /// The event that is emitted when tokens are deposited into a Vault + /// + access(all) event TokensDeposited(amount: Int, to: Address?) + + /// Provider + /// + /// The interface that enforces the requirements for withdrawing + /// tokens from the implementing type. + /// + /// It does not enforce requirements on 'balance' here, + /// because it leaves open the possibility of creating custom providers + /// that do not necessarily need their own balance. + /// + access(all) resource interface Provider { + + /// withdraw subtracts tokens from the owner's Vault + /// and returns a Vault with the removed tokens. + /// + /// The function's access level is public, but this is not a problem + /// because only the owner storing the resource in their account + /// can initially call this function. + /// + /// The owner may grant other accounts access by creating a private + /// capability that allows specific other users to access + /// the provider resource through a reference. + /// + /// The owner may also grant all accounts access by creating a public + /// capability that allows all users to access the provider + /// resource through a reference. + /// + access(all) fun withdraw(amount: Int): @{Vault} { + post { + // 'result' refers to the return value + result.balance == amount: + "Withdrawal amount must be the same as the balance of the withdrawn Vault" + } + } + } + + /// Receiver + /// + /// The interface that enforces the requirements for depositing + /// tokens into the implementing type. + /// + /// We do not include a condition that checks the balance because + /// we want to give users the ability to make custom receivers that + /// can do custom things with the tokens, like split them up and + /// send them to different places. + /// + access(all) resource interface Receiver { + + /// deposit takes a Vault and deposits it into the implementing resource type + /// + access(all) fun deposit(from: @{Vault}) + } + + /// Balance + /// + /// The interface that contains the 'balance' field of the Vault + /// and enforces that when new Vaults are created, the balance + /// is initialized correctly. + /// + access(all) resource interface Balance { + + /// The total balance of a vault + /// + access(all) var balance: Int + + init(balance: Int) { + post { + self.balance == balance: + "Balance must be initialized to the initial balance" + } + } + } + + /// Vault + /// + /// The resource that contains the functions to send and receive tokens. + /// + access(all) resource interface Vault: Provider, Receiver, Balance { + + // The declaration of a concrete type in a contract interface means that + // every Fungible Token contract that implements the FungibleToken interface + // must define a concrete 'Vault' resource that conforms to the 'Provider', 'Receiver', + // and 'Balance' interfaces, and declares their required fields and functions + + /// The total balance of the vault + /// + access(all) var balance: Int + + // The conforming type must declare an initializer + // that allows prioviding the initial balance of the Vault + // + init(balance: Int) + + /// withdraw subtracts 'amount' from the Vault's balance + /// and returns a new Vault with the subtracted balance + /// + access(all) fun withdraw(amount: Int): @{Vault} { + pre { + self.balance >= amount: + "Amount withdrawn must be less than or equal than the balance of the Vault" + } + post { + // use the special function 'before' to get the value of the 'balance' field + // at the beginning of the function execution + // + self.balance == before(self.balance) - amount: + "New Vault balance must be the difference of the previous balance and the withdrawn Vault" + } + } + + /// deposit takes a Vault and adds its balance to the balance of this Vault + /// + access(all) fun deposit(from: @{Vault}) { + post { + self.balance == before(self.balance) + before(from.balance): + "New Vault balance must be the sum of the previous balance and the deposited Vault" + } + } + } + + /// createEmptyVault allows any user to create a new Vault that has a zero balance + /// + access(all) fun createEmptyVault(): @{Vault} { + post { + result.balance == 0: "The newly created Vault must have zero balance" + } + } +} +` + +const realFlowContract = ` +import FungibleToken from 0x1 + +access(all) contract FlowToken: FungibleToken { + + // Total supply of Flow tokens in existence + access(all) var totalSupply: Int + + // Vault + // + // Each user stores an instance of only the Vault in their storage + // The functions in the Vault and governed by the pre and post conditions + // in FungibleToken when they are called. + // The checks happen at runtime whenever a function is called. + // + // Resources can only be created in the context of the contract that they + // are defined in, so there is no way for a malicious user to create Vaults + // out of thin air. A special Minter resource needs to be defined to mint + // new tokens. + // + access(all) resource Vault: FungibleToken.Vault { + + // holds the balance of a users tokens + access(all) var balance: Int + + // initialize the balance at resource creation time + init(balance: Int) { + self.balance = balance + } + + // withdraw + // + // Function that takes an integer amount as an argument + // and withdraws that amount from the Vault. + // It creates a new temporary Vault that is used to hold + // the money that is being transferred. It returns the newly + // created Vault to the context that called so it can be deposited + // elsewhere. + // + access(all) fun withdraw(amount: Int): @{FungibleToken.Vault} { + self.balance = self.balance - amount + return <-create Vault(balance: amount) + } + + // deposit + // + // Function that takes a Vault object as an argument and adds + // its balance to the balance of the owners Vault. + // It is allowed to destroy the sent Vault because the Vault + // was a temporary holder of the tokens. The Vault's balance has + // been consumed and therefore can be destroyed. + access(all) fun deposit(from: @{FungibleToken.Vault}) { + let vault <- from as! @FlowToken.Vault + self.balance = self.balance + vault.balance + vault.balance = 0 + destroy vault + } + } + + // createEmptyVault + // + // Function that creates a new Vault with a balance of zero + // and returns it to the calling context. A user must call this function + // and store the returned Vault in their storage in order to allow their + // account to be able to receive deposits of this token type. + // + access(all) fun createEmptyVault(): @{FungibleToken.Vault} { + return <-create Vault(balance: 0) + } + + access(all) resource Administrator { + // createNewMinter + // + // Function that creates and returns a new minter resource + // + access(all) fun createNewMinter(allowedAmount: Int): @Minter { + return <-create Minter(allowedAmount: allowedAmount) + } + + // createNewBurner + // + // Function that creates and returns a new burner resource + // + access(all) fun createNewBurner(): @Burner { + return <-create Burner() + } + } + + // Minter + // + // Resource object that token admin accounts can hold to mint new tokens. + // + access(all) resource Minter { + + // the amount of tokens that the minter is allowed to mint + access(all) var allowedAmount: Int + + // mintTokens + // + // Function that mints new tokens, adds them to the total supply, + // and returns them to the calling context. + // + access(all) fun mintTokens(amount: Int): @FlowToken.Vault { + pre { + amount > 0: "Amount minted must be greater than zero" + amount <= self.allowedAmount: "Amount minted must be less than the allowed amount" + } + FlowToken.totalSupply = FlowToken.totalSupply + amount + self.allowedAmount = self.allowedAmount - amount + return <-create Vault(balance: amount) + } + + init(allowedAmount: Int) { + self.allowedAmount = allowedAmount + } + } + + // Burner + // + // Resource object that token admin accounts can hold to burn tokens. + // + access(all) resource Burner { + + // burnTokens + // + // Function that destroys a Vault instance, effectively burning the tokens. + // + // Note: the burned tokens are automatically subtracted from the + // total supply in the Vault destructor. + // + access(all) fun burnTokens(from: @{FungibleToken.Vault}) { + let vault <- from as! @FlowToken.Vault + let amount = vault.balance + destroy vault + } + } + + init(adminAccount: auth(Storage, Capabilities) &Account) { + self.totalSupply = 0 + + // Create the Vault with the total supply of tokens and save it in storage + // + let vault <- create Vault(balance: self.totalSupply) + + adminAccount.storage.save(<-vault, to: /storage/flowTokenVault) + + // Create a public capability to the stored Vault that only exposes + // the 'deposit' method through the 'Receiver' interface + // + let receiverCapability = adminAccount.capabilities.storage.issue<&FlowToken.Vault>(/storage/flowTokenVault) + adminAccount.capabilities.publish(receiverCapability, at: /public/flowTokenReceiver) + + // Create a public capability to the stored Vault that only exposes + // the 'balance' field through the 'Balance' interface + // + let balanceCapability = adminAccount.capabilities.storage.issue<&FlowToken.Vault>(/storage/flowTokenVault) + adminAccount.capabilities.publish(balanceCapability, at: /public/flowTokenBalance) + + let admin <- create Administrator() + adminAccount.storage.save(<-admin, to: /storage/flowTokenAdmin) + } +} +` + +const realSetupFlowTokenAccountTransaction = ` +import FungibleToken from 0x1 +import FlowToken from 0x1 + +transaction { + + prepare(signer: auth(BorrowValue, IssueStorageCapabilityController, PublishCapability, SaveValue) &Account) { + + var storagePath = /storage/flowTokenVault + + if signer.storage.borrow<&FlowToken.Vault>(from: storagePath) != nil { + return + } + + // Create a new flowToken Vault and put it in storage + signer.storage.save(<-FlowToken.createEmptyVault(), to: storagePath) + + // Create a public capability to the Vault that only exposes + // the deposit function through the Receiver interface + let vaultCap = signer.capabilities.storage.issue<&FlowToken.Vault>(storagePath) + + signer.capabilities.publish( + vaultCap, + at: /public/flowTokenReceiver + ) + + // Create a public capability to the Vault that only exposes + // the balance field through the Balance interface + let balanceCap = signer.capabilities.storage.issue<&FlowToken.Vault>(storagePath) + + signer.capabilities.publish( + balanceCap, + at: /public/flowTokenBalance + ) + } +} +` + +const realFlowTokenTransferTransaction = ` +import FungibleToken from 0x1 +import FlowToken from 0x1 + +transaction(amount: Int, to: Address) { + let sentVault: @{FungibleToken.Vault} + + prepare(signer: auth(BorrowValue) &Account) { + // Get a reference to the signer's stored vault + let vaultRef = signer.storage.borrow<&FlowToken.Vault>(from: /storage/flowTokenVault) + ?? panic("Could not borrow reference to the owner's Vault!") + + // Withdraw tokens from the signer's stored vault + self.sentVault <- vaultRef.withdraw(amount: amount) + } + + execute { + // Get a reference to the recipient's Receiver + let receiverRef = getAccount(to) + .capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver) + .borrow() + ?? panic("Could not borrow receiver reference to the recipient's Vault") + + // Deposit the withdrawn tokens in the recipient's receiver + receiverRef.deposit(from: <-self.sentVault) + } +} +` + +const realMintFlowTokenTransaction = ` +import FungibleToken from 0x1 +import FlowToken from 0x1 + +transaction(recipient: Address, amount: Int) { + + /// Reference to the FlowToken Minter Resource object + let tokenAdmin: &FlowToken.Administrator + + /// Reference to the Fungible Token Receiver of the recipient + let tokenReceiver: &{FungibleToken.Receiver} + + prepare(signer: auth(BorrowValue) &Account) { + self.tokenAdmin = signer.storage + .borrow<&FlowToken.Administrator>(from: /storage/flowTokenAdmin) + ?? panic("Signer is not the token admin") + + self.tokenReceiver = getAccount(recipient) + .capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver) + .borrow() + ?? panic("Unable to borrow receiver reference") + } + + execute { + let minter <- self.tokenAdmin.createNewMinter(allowedAmount: amount) + let mintedVault <- minter.mintTokens(amount: amount) + + self.tokenReceiver.deposit(from: <-mintedVault) + + destroy minter + } +} +` + +const realFlowTokenBalanceScript = ` +import FungibleToken from 0x1 +import FlowToken from 0x1 + +access(all) fun main(account: Address): Int { + + let vaultRef = getAccount(account) + .capabilities.get<&FlowToken.Vault>(/public/flowTokenBalance) + .borrow() + ?? panic("Could not borrow Balance reference to the Vault") + + return vaultRef.balance +} +` diff --git a/bbq/vm/test/ft_test.go b/bbq/vm/test/ft_test.go new file mode 100644 index 0000000000..dc70abc385 --- /dev/null +++ b/bbq/vm/test/ft_test.go @@ -0,0 +1,359 @@ +/* + * 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 test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/onflow/cadence/bbq" + "github.com/onflow/cadence/bbq/opcode" + "github.com/onflow/cadence/bbq/vm" + "github.com/onflow/cadence/common" + "github.com/onflow/cadence/interpreter" + "github.com/onflow/cadence/sema" + "github.com/onflow/cadence/test_utils/runtime_utils" +) + +func TestFTTransfer(t *testing.T) { + + // ---- Deploy FT Contract ----- + + storage := interpreter.NewInMemoryStorage(nil) + programs := map[common.Location]compiledProgram{} + + typeLoader := func(location common.Location, typeID interpreter.TypeID) sema.CompositeKindedType { + program, ok := programs[location] + if !ok { + panic(fmt.Errorf("cannot find elaboration for: %s", location)) + } + elaboration := program.Elaboration + compositeType := elaboration.CompositeType(typeID) + if compositeType != nil { + return compositeType + } + + return elaboration.InterfaceType(typeID) + } + + contractsAddress := common.MustBytesToAddress([]byte{0x1}) + senderAddress := common.MustBytesToAddress([]byte{0x2}) + receiverAddress := common.MustBytesToAddress([]byte{0x3}) + + txLocation := runtime_utils.NewTransactionLocationGenerator() + scriptLocation := runtime_utils.NewScriptLocationGenerator() + + ftLocation := common.NewAddressLocation(nil, contractsAddress, "FungibleToken") + + ftContractProgram := compileCode(t, realFungibleTokenContractInterface, ftLocation, programs) + printProgram("FungibleToken", ftContractProgram) + + // ----- Deploy FlowToken Contract ----- + + flowTokenLocation := common.NewAddressLocation(nil, contractsAddress, "FlowToken") + + flowTokenProgram := compileCode(t, realFlowContract, flowTokenLocation, programs) + printProgram("FlowToken", flowTokenProgram) + + config := &vm.Config{ + Storage: storage, + AccountHandler: &testAccountHandler{}, + TypeLoader: typeLoader, + } + + flowTokenVM := vm.NewVM( + flowTokenLocation, + flowTokenProgram, + config, + ) + + authAccount := vm.NewAuthAccountReferenceValue(config, contractsAddress) + flowTokenContractValue, err := flowTokenVM.InitializeContract(authAccount) + require.NoError(t, err) + + // ----- Run setup account transaction ----- + + vmConfig := &vm.Config{ + Storage: storage, + ImportHandler: func(location common.Location) *bbq.Program[opcode.Instruction] { + imported, ok := programs[location] + if !ok { + return nil + } + return imported.Program + }, + ContractValueHandler: func(_ *vm.Config, location common.Location) *vm.CompositeValue { + switch location { + case ftLocation: + // interface + return nil + case flowTokenLocation: + return flowTokenContractValue + default: + assert.FailNow(t, "invalid location") + return nil + } + }, + + AccountHandler: &testAccountHandler{}, + + TypeLoader: typeLoader, + } + + for _, address := range []common.Address{ + senderAddress, + receiverAddress, + } { + program := compileCode(t, realSetupFlowTokenAccountTransaction, nil, programs) + + setupTxVM := vm.NewVM(txLocation(), program, vmConfig) + + authorizer := vm.NewAuthAccountReferenceValue(vmConfig, address) + err = setupTxVM.ExecuteTransaction(nil, authorizer) + require.NoError(t, err) + require.Equal(t, 0, setupTxVM.StackSize()) + } + + // Mint FLOW to sender + + program := compileCode(t, realMintFlowTokenTransaction, nil, programs) + printProgram("Setup FlowToken Tx", program) + + mintTxVM := vm.NewVM(txLocation(), program, vmConfig) + + total := int64(1000000) + + mintTxArgs := []vm.Value{ + vm.AddressValue(senderAddress), + vm.NewIntValue(total), + } + + mintTxAuthorizer := vm.NewAuthAccountReferenceValue(vmConfig, contractsAddress) + err = mintTxVM.ExecuteTransaction(mintTxArgs, mintTxAuthorizer) + require.NoError(t, err) + require.Equal(t, 0, mintTxVM.StackSize()) + + // ----- Run token transfer transaction ----- + + tokenTransferTxProgram := compileCode(t, realFlowTokenTransferTransaction, nil, programs) + printProgram("FT Transfer Tx", tokenTransferTxProgram) + + tokenTransferTxVM := vm.NewVM(txLocation(), tokenTransferTxProgram, vmConfig) + + transferAmount := int64(1) + + tokenTransferTxArgs := []vm.Value{ + vm.NewIntValue(transferAmount), + vm.AddressValue(receiverAddress), + } + + tokenTransferTxAuthorizer := vm.NewAuthAccountReferenceValue(vmConfig, senderAddress) + err = tokenTransferTxVM.ExecuteTransaction(tokenTransferTxArgs, tokenTransferTxAuthorizer) + require.NoError(t, err) + require.Equal(t, 0, tokenTransferTxVM.StackSize()) + + // Run validation scripts + + for _, address := range []common.Address{ + senderAddress, + receiverAddress, + } { + program := compileCode(t, realFlowTokenBalanceScript, nil, programs) + + validationScriptVM := vm.NewVM(scriptLocation(), program, vmConfig) + + addressValue := vm.AddressValue(address) + result, err := validationScriptVM.Invoke("main", addressValue) + require.NoError(t, err) + require.Equal(t, 0, validationScriptVM.StackSize()) + + if address == senderAddress { + assert.Equal(t, vm.NewIntValue(total-transferAmount), result) + } else { + assert.Equal(t, vm.NewIntValue(transferAmount), result) + } + } +} + +func BenchmarkFTTransfer(b *testing.B) { + + // ---- Deploy FT Contract ----- + + storage := interpreter.NewInMemoryStorage(nil) + programs := map[common.Location]compiledProgram{} + + typeLoader := func(location common.Location, typeID interpreter.TypeID) sema.CompositeKindedType { + program, ok := programs[location] + if !ok { + panic(fmt.Errorf("cannot find elaboration for: %s", location)) + } + elaboration := program.Elaboration + compositeType := elaboration.CompositeType(typeID) + if compositeType != nil { + return compositeType + } + + return elaboration.InterfaceType(typeID) + } + + contractsAddress := common.MustBytesToAddress([]byte{0x1}) + senderAddress := common.MustBytesToAddress([]byte{0x2}) + receiverAddress := common.MustBytesToAddress([]byte{0x3}) + + txLocation := runtime_utils.NewTransactionLocationGenerator() + + ftLocation := common.NewAddressLocation(nil, contractsAddress, "FungibleToken") + _ = compileCode(b, realFungibleTokenContractInterface, ftLocation, programs) + + // ----- Deploy FlowToken Contract ----- + + flowTokenLocation := common.NewAddressLocation(nil, contractsAddress, "FlowToken") + flowTokenProgram := compileCode(b, realFlowContract, flowTokenLocation, programs) + + config := &vm.Config{ + Storage: storage, + AccountHandler: &testAccountHandler{}, + TypeLoader: typeLoader, + } + + flowTokenVM := vm.NewVM( + flowTokenLocation, + flowTokenProgram, + config, + ) + + authAccount := vm.NewAuthAccountReferenceValue(config, contractsAddress) + + flowTokenContractValue, err := flowTokenVM.InitializeContract(authAccount) + require.NoError(b, err) + + // ----- Run setup account transaction ----- + + vmConfig := &vm.Config{ + Storage: storage, + ImportHandler: func(location common.Location) *bbq.Program[opcode.Instruction] { + imported, ok := programs[location] + if !ok { + return nil + } + return imported.Program + }, + ContractValueHandler: func(_ *vm.Config, location common.Location) *vm.CompositeValue { + switch location { + case ftLocation: + // interface + return nil + case flowTokenLocation: + return flowTokenContractValue + default: + assert.FailNow(b, "invalid location") + return nil + } + }, + + AccountHandler: &testAccountHandler{}, + + TypeLoader: typeLoader, + } + + for _, address := range []common.Address{ + senderAddress, + receiverAddress, + } { + program := compileCode(b, realSetupFlowTokenAccountTransaction, nil, programs) + + setupTxVM := vm.NewVM(txLocation(), program, vmConfig) + + authorizer := vm.NewAuthAccountReferenceValue(vmConfig, address) + err = setupTxVM.ExecuteTransaction(nil, authorizer) + require.NoError(b, err) + require.Equal(b, 0, setupTxVM.StackSize()) + } + + // Mint FLOW to sender + + program := compileCode(b, realMintFlowTokenTransaction, nil, programs) + + mintTxVM := vm.NewVM(txLocation(), program, vmConfig) + + total := int64(1000000) + + mintTxArgs := []vm.Value{ + vm.AddressValue(senderAddress), + vm.NewIntValue(total), + } + + mintTxAuthorizer := vm.NewAuthAccountReferenceValue(vmConfig, contractsAddress) + err = mintTxVM.ExecuteTransaction(mintTxArgs, mintTxAuthorizer) + require.NoError(b, err) + require.Equal(b, 0, mintTxVM.StackSize()) + + // ----- Run token transfer transaction ----- + + transferAmount := int64(1) + + tokenTransferTxArgs := []vm.Value{ + vm.NewIntValue(transferAmount), + vm.AddressValue(receiverAddress), + } + + tokenTransferTxAuthorizer := vm.NewAuthAccountReferenceValue(vmConfig, senderAddress) + + tokenTransferTxChecker := parseAndCheck(b, realFlowTokenTransferTransaction, nil, programs) + tokenTransferTxProgram := compile(b, tokenTransferTxChecker, programs) + tokenTransferTxVM := vm.NewVM(txLocation(), tokenTransferTxProgram, vmConfig) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + err = tokenTransferTxVM.ExecuteTransaction(tokenTransferTxArgs, tokenTransferTxAuthorizer) + require.NoError(b, err) + } + + b.StopTimer() + + // Run validation scripts + + // actual transfer amount = (transfer amount in one tx) * (number of time the tx/benchmark runs) + actualTransferAmount := transferAmount * int64(b.N) + + for _, address := range []common.Address{ + senderAddress, + receiverAddress, + } { + program := compileCode(b, realFlowTokenBalanceScript, nil, programs) + + validationScriptVM := vm.NewVM(scriptLocation(), program, vmConfig) + + addressValue := vm.AddressValue(address) + result, err := validationScriptVM.Invoke("main", addressValue) + require.NoError(b, err) + require.Equal(b, 0, validationScriptVM.StackSize()) + + if address == senderAddress { + assert.Equal(b, vm.NewIntValue(total-actualTransferAmount), result) + } else { + assert.Equal(b, vm.NewIntValue(actualTransferAmount), result) + } + } +} diff --git a/bbq/vm/test/interpreter_test.go b/bbq/vm/test/interpreter_test.go new file mode 100644 index 0000000000..9de6f0c0b2 --- /dev/null +++ b/bbq/vm/test/interpreter_test.go @@ -0,0 +1,1116 @@ +/* + * 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 test + +import ( + "encoding/hex" + "fmt" + "strings" + "testing" + + "github.com/onflow/cadence/test_utils/interpreter_utils" + "github.com/onflow/cadence/test_utils/runtime_utils" + "github.com/onflow/cadence/test_utils/sema_utils" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/onflow/cadence" + "github.com/onflow/cadence/activations" + "github.com/onflow/cadence/ast" + "github.com/onflow/cadence/common" + "github.com/onflow/cadence/encoding/json" + "github.com/onflow/cadence/interpreter" + "github.com/onflow/cadence/pretty" + "github.com/onflow/cadence/runtime" + "github.com/onflow/cadence/sema" + "github.com/onflow/cadence/stdlib" +) + +type ParseCheckAndInterpretOptions struct { + Config *interpreter.Config + CheckerConfig *sema.Config + HandleCheckerError func(error) +} + +func parseCheckAndInterpretWithOptions( + t testing.TB, + code string, + location common.Location, + options ParseCheckAndInterpretOptions, +) ( + inter *interpreter.Interpreter, + err error, +) { + return parseCheckAndInterpretWithOptionsAndMemoryMetering(t, code, location, options, nil) +} + +func parseCheckAndInterpretWithOptionsAndMemoryMetering( + t testing.TB, + code string, + location common.Location, + options ParseCheckAndInterpretOptions, + memoryGauge common.MemoryGauge, +) ( + inter *interpreter.Interpreter, + err error, +) { + + checker, err := sema_utils.ParseAndCheckWithOptionsAndMemoryMetering(t, + code, + sema_utils.ParseAndCheckOptions{ + Location: location, + Config: options.CheckerConfig, + }, + memoryGauge, + ) + + if options.HandleCheckerError != nil { + options.HandleCheckerError(err) + } else if !assert.NoError(t, err) { + var sb strings.Builder + location := checker.Location + printErr := pretty.NewErrorPrettyPrinter(&sb, true). + PrettyPrintError(err, location, map[common.Location][]byte{location: []byte(code)}) + if printErr != nil { + panic(printErr) + } + assert.Fail(t, sb.String()) + return nil, err + } + + var uuid uint64 = 0 + + var config interpreter.Config + if options.Config != nil { + config = *options.Config + } + + if config.UUIDHandler == nil { + config.UUIDHandler = func() (uint64, error) { + uuid++ + return uuid, nil + } + } + if config.Storage == nil { + config.Storage = interpreter.NewInMemoryStorage(memoryGauge) + } + + if memoryGauge != nil && config.MemoryGauge == nil { + config.MemoryGauge = memoryGauge + } + + inter, err = interpreter.NewInterpreter( + interpreter.ProgramFromChecker(checker), + checker.Location, + &config, + ) + require.NoError(t, err) + + err = inter.Interpret() + + if err == nil { + + // recover internal panics and return them as an error + defer inter.RecoverErrors(func(internalErr error) { + err = internalErr + }) + + // Contract declarations are evaluated lazily, + // so force the contract value handler to be called + + for _, compositeDeclaration := range checker.Program.CompositeDeclarations() { + if compositeDeclaration.CompositeKind != common.CompositeKindContract { + continue + } + + contractVariable := inter.Globals.Get(compositeDeclaration.Identifier.Identifier) + + _ = contractVariable.GetValue(inter) + } + } + + return inter, err +} + +func TestInterpreterFTTransfer(t *testing.T) { + + // ---- Deploy FT Contract ----- + + storage := interpreter.NewInMemoryStorage(nil) + + contractsAddress := common.MustBytesToAddress([]byte{0x1}) + senderAddress := common.MustBytesToAddress([]byte{0x2}) + receiverAddress := common.MustBytesToAddress([]byte{0x3}) + + flowTokenLocation := common.NewAddressLocation(nil, contractsAddress, "FlowToken") + ftLocation := common.NewAddressLocation(nil, contractsAddress, "FungibleToken") + + subInterpreters := map[common.Location]*interpreter.Interpreter{} + codes := map[common.Location][]byte{ + ftLocation: []byte(realFungibleTokenContractInterface), + } + + txLocation := runtime_utils.NewTransactionLocationGenerator() + scriptLocation := runtime_utils.NewScriptLocationGenerator() + + var signer interpreter.Value + var flowTokenContractValue *interpreter.CompositeValue + + accountHandler := &testAccountHandler{ + getAccountContractCode: func(location common.AddressLocation) ([]byte, error) { + code, ok := codes[location] + if !ok { + return nil, nil + // return nil, fmt.Errorf("cannot find code for %s", location) + } + + return code, nil + }, + updateAccountContractCode: func(location common.AddressLocation, code []byte) error { + codes[location] = code + return nil + }, + contractUpdateRecorded: func(location common.AddressLocation) bool { + return false + }, + interpretContract: func( + location common.AddressLocation, + program *interpreter.Program, + name string, + invocation stdlib.DeployedContractConstructorInvocation, + ) (*interpreter.CompositeValue, error) { + if location == flowTokenLocation { + return flowTokenContractValue, nil + } + return nil, fmt.Errorf("cannot interpret contract %s", location) + }, + temporarilyRecordCode: func(location common.AddressLocation, code []byte) { + // do nothing + }, + emitEvent: func(*interpreter.Interpreter, interpreter.LocationRange, *sema.CompositeType, []interpreter.Value) { + // do nothing + }, + recordContractUpdate: func(location common.AddressLocation, value *interpreter.CompositeValue) { + // do nothing + }, + } + + baseActivation := activations.NewActivation(nil, interpreter.BaseActivation) + interpreter.Declare(baseActivation, stdlib.PanicFunction) + interpreter.Declare(baseActivation, stdlib.NewGetAccountFunction(accountHandler)) + + checkerConfig := &sema.Config{ + ImportHandler: func(checker *sema.Checker, location common.Location, importRange ast.Range) (sema.Import, error) { + imported, ok := subInterpreters[location] + if !ok { + return nil, fmt.Errorf("cannot find contract in location %s", location) + } + + return sema.ElaborationImport{ + Elaboration: imported.Program.Elaboration, + }, nil + }, + BaseValueActivationHandler: baseValueActivation, + LocationHandler: singleIdentifierLocationResolver(t), + } + + interConfig := &interpreter.Config{ + Storage: storage, + BaseActivationHandler: func(_ common.Location) *interpreter.VariableActivation { + return baseActivation + }, + ImportLocationHandler: func(inter *interpreter.Interpreter, location common.Location) interpreter.Import { + imported, ok := subInterpreters[location] + if !ok { + panic(fmt.Errorf("cannot find contract in location %s", location)) + } + + return interpreter.InterpreterImport{ + Interpreter: imported, + } + }, + ContractValueHandler: func( + inter *interpreter.Interpreter, + compositeType *sema.CompositeType, + constructorGenerator func(common.Address) *interpreter.HostFunctionValue, + invocationRange ast.Range, + ) interpreter.ContractValue { + + constructor := constructorGenerator(common.ZeroAddress) + + value, err := inter.InvokeFunctionValue( + constructor, + []interpreter.Value{signer}, + []sema.Type{ + sema.FullyEntitledAccountReferenceType, + }, + []sema.Type{ + sema.FullyEntitledAccountReferenceType, + }, + compositeType, + ast.Range{}, + ) + if err != nil { + panic(err) + } + + flowTokenContractValue = value.(*interpreter.CompositeValue) + return flowTokenContractValue + }, + CapabilityBorrowHandler: func( + inter *interpreter.Interpreter, + locationRange interpreter.LocationRange, + address interpreter.AddressValue, + capabilityID interpreter.UInt64Value, + wantedBorrowType *sema.ReferenceType, + capabilityBorrowType *sema.ReferenceType, + ) interpreter.ReferenceValue { + return stdlib.BorrowCapabilityController( + inter, + locationRange, + address, + capabilityID, + wantedBorrowType, + capabilityBorrowType, + accountHandler, + ) + }, + } + + accountHandler.parseAndCheckProgram = + func(code []byte, location common.Location, getAndSetProgram bool) (*interpreter.Program, error) { + if subInterpreter, ok := subInterpreters[location]; ok { + return subInterpreter.Program, nil + } + + inter, err := parseCheckAndInterpretWithOptions( + t, + string(code), + location, + ParseCheckAndInterpretOptions{ + Config: interConfig, + CheckerConfig: checkerConfig, + }, + ) + + if err != nil { + return nil, err + } + + subInterpreters[location] = inter + + return inter.Program, err + } + + // ----- Parse and Check FungibleToken Contract interface ----- + + inter, err := parseCheckAndInterpretWithOptions( + t, + realFungibleTokenContractInterface, + ftLocation, + ParseCheckAndInterpretOptions{ + Config: interConfig, + CheckerConfig: checkerConfig, + }, + ) + require.NoError(t, err) + subInterpreters[ftLocation] = inter + + // ----- Deploy FlowToken Contract ----- + + tx := fmt.Sprintf(` + transaction { + prepare(signer: auth(Storage, Capabilities, Contracts) &Account) { + signer.contracts.add(name: "FlowToken", code: "%s".decodeHex(), signer) + } + }`, + hex.EncodeToString([]byte(realFlowContract)), + ) + + inter, err = parseCheckAndInterpretWithOptions( + t, + tx, + txLocation(), + ParseCheckAndInterpretOptions{ + Config: interConfig, + CheckerConfig: checkerConfig, + }, + ) + require.NoError(t, err) + + signer = stdlib.NewAccountReferenceValue( + inter, + accountHandler, + interpreter.AddressValue(contractsAddress), + interpreter.FullyEntitledAccountAccess, + interpreter.EmptyLocationRange, + ) + + err = inter.InvokeTransaction(0, signer) + require.NoError(t, err) + + // ----- Run setup account transaction ----- + + authorization := sema.NewEntitlementSetAccess( + []*sema.EntitlementType{ + sema.BorrowValueType, + sema.IssueStorageCapabilityControllerType, + sema.PublishCapabilityType, + sema.SaveValueType, + }, + sema.Conjunction, + ) + + for _, address := range []common.Address{ + senderAddress, + receiverAddress, + } { + inter, err := parseCheckAndInterpretWithOptions( + t, + realSetupFlowTokenAccountTransaction, + txLocation(), + ParseCheckAndInterpretOptions{ + Config: interConfig, + CheckerConfig: checkerConfig, + }, + ) + require.NoError(t, err) + + signer = stdlib.NewAccountReferenceValue( + inter, + accountHandler, + interpreter.AddressValue(address), + interpreter.ConvertSemaAccessToStaticAuthorization(nil, authorization), + interpreter.EmptyLocationRange, + ) + + err = inter.InvokeTransaction(0, signer) + require.NoError(t, err) + } + + // Mint FLOW to sender + + total := int64(1000000) + + inter, err = parseCheckAndInterpretWithOptions( + t, + realMintFlowTokenTransaction, + txLocation(), + ParseCheckAndInterpretOptions{ + Config: interConfig, + CheckerConfig: checkerConfig, + }, + ) + require.NoError(t, err) + + signer = stdlib.NewAccountReferenceValue( + inter, + accountHandler, + interpreter.AddressValue(contractsAddress), + interpreter.ConvertSemaAccessToStaticAuthorization(nil, authorization), + interpreter.EmptyLocationRange, + ) + + err = inter.InvokeTransaction( + 0, + interpreter.AddressValue(senderAddress), + interpreter.NewUnmeteredIntValueFromInt64(total), + signer, + ) + require.NoError(t, err) + + // ----- Run token transfer transaction ----- + + transferAmount := int64(1) + + inter, err = parseCheckAndInterpretWithOptions( + t, + realFlowTokenTransferTransaction, + txLocation(), + ParseCheckAndInterpretOptions{ + Config: interConfig, + CheckerConfig: checkerConfig, + }, + ) + require.NoError(t, err) + + signer = stdlib.NewAccountReferenceValue( + inter, + accountHandler, + interpreter.AddressValue(senderAddress), + interpreter.ConvertSemaAccessToStaticAuthorization(nil, authorization), + interpreter.EmptyLocationRange, + ) + + err = inter.InvokeTransaction( + 0, + interpreter.NewUnmeteredIntValueFromInt64(transferAmount), + interpreter.AddressValue(receiverAddress), + signer, + ) + require.NoError(t, err) + + // Run validation scripts + + for _, address := range []common.Address{ + senderAddress, + receiverAddress, + } { + inter, err = parseCheckAndInterpretWithOptions( + t, + realFlowTokenBalanceScript, + scriptLocation(), + ParseCheckAndInterpretOptions{ + Config: interConfig, + CheckerConfig: checkerConfig, + }, + ) + require.NoError(t, err) + + result, err := inter.Invoke( + "main", + interpreter.AddressValue(address), + ) + require.NoError(t, err) + + if address == senderAddress { + assert.Equal(t, interpreter.NewUnmeteredIntValueFromInt64(total-transferAmount), result) + } else { + assert.Equal(t, interpreter.NewUnmeteredIntValueFromInt64(transferAmount), result) + } + } +} + +func BenchmarkInterpreterFTTransfer(b *testing.B) { + + // ---- Deploy FT Contract ----- + + storage := interpreter.NewInMemoryStorage(nil) + + contractsAddress := common.MustBytesToAddress([]byte{0x1}) + senderAddress := common.MustBytesToAddress([]byte{0x2}) + receiverAddress := common.MustBytesToAddress([]byte{0x3}) + + flowTokenLocation := common.NewAddressLocation(nil, contractsAddress, "FlowToken") + ftLocation := common.NewAddressLocation(nil, contractsAddress, "FungibleToken") + + subInterpreters := map[common.Location]*interpreter.Interpreter{} + codes := map[common.Location][]byte{ + ftLocation: []byte(realFungibleTokenContractInterface), + } + + txLocation := runtime_utils.NewTransactionLocationGenerator() + + var signer interpreter.Value + var flowTokenContractValue *interpreter.CompositeValue + + accountHandler := &testAccountHandler{ + getAccountContractCode: func(location common.AddressLocation) ([]byte, error) { + code, ok := codes[location] + if !ok { + return nil, nil + // return nil, fmt.Errorf("cannot find code for %s", location) + } + + return code, nil + }, + updateAccountContractCode: func(location common.AddressLocation, code []byte) error { + codes[location] = code + return nil + }, + contractUpdateRecorded: func(location common.AddressLocation) bool { + return false + }, + interpretContract: func( + location common.AddressLocation, + program *interpreter.Program, + name string, + invocation stdlib.DeployedContractConstructorInvocation, + ) (*interpreter.CompositeValue, error) { + if location == flowTokenLocation { + return flowTokenContractValue, nil + } + return nil, fmt.Errorf("cannot interpret contract %s", location) + }, + temporarilyRecordCode: func(location common.AddressLocation, code []byte) { + // do nothing + }, + emitEvent: func(*interpreter.Interpreter, interpreter.LocationRange, *sema.CompositeType, []interpreter.Value) { + // do nothing + }, + recordContractUpdate: func(location common.AddressLocation, value *interpreter.CompositeValue) { + // do nothing + }, + } + + baseActivation := activations.NewActivation(nil, interpreter.BaseActivation) + interpreter.Declare(baseActivation, stdlib.PanicFunction) + interpreter.Declare(baseActivation, stdlib.NewGetAccountFunction(accountHandler)) + + checkerConfig := &sema.Config{ + ImportHandler: func(checker *sema.Checker, location common.Location, importRange ast.Range) (sema.Import, error) { + imported, ok := subInterpreters[location] + if !ok { + return nil, fmt.Errorf("cannot find contract in location %s", location) + } + + return sema.ElaborationImport{ + Elaboration: imported.Program.Elaboration, + }, nil + }, + BaseValueActivationHandler: baseValueActivation, + LocationHandler: singleIdentifierLocationResolver(b), + } + + interConfig := &interpreter.Config{ + Storage: storage, + BaseActivationHandler: func(_ common.Location) *interpreter.VariableActivation { + return baseActivation + }, + ImportLocationHandler: func(inter *interpreter.Interpreter, location common.Location) interpreter.Import { + imported, ok := subInterpreters[location] + if !ok { + panic(fmt.Errorf("cannot find contract in location %s", location)) + } + + return interpreter.InterpreterImport{ + Interpreter: imported, + } + }, + ContractValueHandler: func( + inter *interpreter.Interpreter, + compositeType *sema.CompositeType, + constructorGenerator func(common.Address) *interpreter.HostFunctionValue, + invocationRange ast.Range, + ) interpreter.ContractValue { + + constructor := constructorGenerator(common.ZeroAddress) + + value, err := inter.InvokeFunctionValue( + constructor, + []interpreter.Value{signer}, + []sema.Type{ + sema.FullyEntitledAccountReferenceType, + }, + []sema.Type{ + sema.FullyEntitledAccountReferenceType, + }, + compositeType, + ast.Range{}, + ) + if err != nil { + panic(err) + } + + flowTokenContractValue = value.(*interpreter.CompositeValue) + return flowTokenContractValue + }, + CapabilityBorrowHandler: func( + inter *interpreter.Interpreter, + locationRange interpreter.LocationRange, + address interpreter.AddressValue, + capabilityID interpreter.UInt64Value, + wantedBorrowType *sema.ReferenceType, + capabilityBorrowType *sema.ReferenceType, + ) interpreter.ReferenceValue { + return stdlib.BorrowCapabilityController( + inter, + locationRange, + address, + capabilityID, + wantedBorrowType, + capabilityBorrowType, + accountHandler, + ) + }, + } + + accountHandler.parseAndCheckProgram = + func(code []byte, location common.Location, getAndSetProgram bool) (*interpreter.Program, error) { + if subInterpreter, ok := subInterpreters[location]; ok { + return subInterpreter.Program, nil + } + + inter, err := parseCheckAndInterpretWithOptions( + b, + string(code), + location, + ParseCheckAndInterpretOptions{ + Config: interConfig, + CheckerConfig: checkerConfig, + }, + ) + + if err != nil { + return nil, err + } + + subInterpreters[location] = inter + + return inter.Program, err + } + + // ----- Parse and Check FungibleToken Contract interface ----- + + inter, err := parseCheckAndInterpretWithOptions( + b, + realFungibleTokenContractInterface, + ftLocation, + ParseCheckAndInterpretOptions{ + Config: interConfig, + CheckerConfig: checkerConfig, + }, + ) + require.NoError(b, err) + subInterpreters[ftLocation] = inter + + // ----- Deploy FlowToken Contract ----- + + tx := fmt.Sprintf(` + transaction { + prepare(signer: auth(Storage, Capabilities, Contracts) &Account) { + signer.contracts.add(name: "FlowToken", code: "%s".decodeHex(), signer) + } + }`, + hex.EncodeToString([]byte(realFlowContract)), + ) + + inter, err = parseCheckAndInterpretWithOptions( + b, + tx, + txLocation(), + ParseCheckAndInterpretOptions{ + Config: interConfig, + CheckerConfig: checkerConfig, + }, + ) + require.NoError(b, err) + + signer = stdlib.NewAccountReferenceValue( + inter, + accountHandler, + interpreter.AddressValue(contractsAddress), + interpreter.FullyEntitledAccountAccess, + interpreter.EmptyLocationRange, + ) + + err = inter.InvokeTransaction(0, signer) + require.NoError(b, err) + + // ----- Run setup account transaction ----- + + authorization := sema.NewEntitlementSetAccess( + []*sema.EntitlementType{ + sema.BorrowValueType, + sema.IssueStorageCapabilityControllerType, + sema.PublishCapabilityType, + sema.SaveValueType, + }, + sema.Conjunction, + ) + + for _, address := range []common.Address{ + senderAddress, + receiverAddress, + } { + inter, err := parseCheckAndInterpretWithOptions( + b, + realSetupFlowTokenAccountTransaction, + txLocation(), + ParseCheckAndInterpretOptions{ + Config: interConfig, + CheckerConfig: checkerConfig, + }, + ) + require.NoError(b, err) + + signer = stdlib.NewAccountReferenceValue( + inter, + accountHandler, + interpreter.AddressValue(address), + interpreter.ConvertSemaAccessToStaticAuthorization(nil, authorization), + interpreter.EmptyLocationRange, + ) + + err = inter.InvokeTransaction(0, signer) + require.NoError(b, err) + } + + // Mint FLOW to sender + + total := int64(1000000) + + inter, err = parseCheckAndInterpretWithOptions( + b, + realMintFlowTokenTransaction, + txLocation(), + ParseCheckAndInterpretOptions{ + Config: interConfig, + CheckerConfig: checkerConfig, + }, + ) + require.NoError(b, err) + + signer = stdlib.NewAccountReferenceValue( + inter, + accountHandler, + interpreter.AddressValue(contractsAddress), + interpreter.ConvertSemaAccessToStaticAuthorization(nil, authorization), + interpreter.EmptyLocationRange, + ) + + err = inter.InvokeTransaction( + 0, + interpreter.AddressValue(senderAddress), + interpreter.NewUnmeteredIntValueFromInt64(total), + signer, + ) + require.NoError(b, err) + + // ----- Run token transfer transaction ----- + + signer = stdlib.NewAccountReferenceValue( + inter, + accountHandler, + interpreter.AddressValue(senderAddress), + interpreter.ConvertSemaAccessToStaticAuthorization(nil, authorization), + interpreter.EmptyLocationRange, + ) + + transferAmount := int64(1) + + amount := interpreter.NewUnmeteredIntValueFromInt64(transferAmount) + receiver := interpreter.AddressValue(receiverAddress) + + inter, err = parseCheckAndInterpretWithOptions( + b, + realFlowTokenTransferTransaction, + txLocation(), + ParseCheckAndInterpretOptions{ + Config: interConfig, + CheckerConfig: checkerConfig, + }, + ) + require.NoError(b, err) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + err = inter.InvokeTransaction( + 0, + amount, + receiver, + signer, + ) + require.NoError(b, err) + } + + b.StopTimer() +} + +func BenchmarkRuntimeFungibleTokenTransfer(b *testing.B) { + + interpreterRuntime := runtime_utils.NewTestInterpreterRuntime() + + contractsAddress := common.MustBytesToAddress([]byte{0x1}) + senderAddress := common.MustBytesToAddress([]byte{0x2}) + receiverAddress := common.MustBytesToAddress([]byte{0x3}) + + accountCodes := map[common.Location][]byte{} + + var events []cadence.Event + + signerAccount := contractsAddress + + runtimeInterface := &runtime_utils.TestRuntimeInterface{ + OnGetCode: func(location common.Location) (bytes []byte, err error) { + return accountCodes[location], nil + }, + Storage: runtime_utils.NewTestLedger(nil, nil), + OnGetSigningAccounts: func() ([]common.Address, error) { + return []common.Address{signerAccount}, nil + }, + OnResolveLocation: runtime_utils.NewSingleIdentifierLocationResolver(b), + OnGetAccountContractCode: func(location common.AddressLocation) (code []byte, err error) { + return accountCodes[location], nil + }, + OnUpdateAccountContractCode: func(location common.AddressLocation, code []byte) error { + accountCodes[location] = code + return nil + }, + OnEmitEvent: func(event cadence.Event) error { + events = append(events, event) + return nil + }, + OnDecodeArgument: func(b []byte, t cadence.Type) (value cadence.Value, err error) { + return json.Decode(nil, b) + }, + } + + environment := runtime.NewBaseInterpreterEnvironment(runtime.Config{}) + + nextTransactionLocation := runtime_utils.NewTransactionLocationGenerator() + + // Deploy Fungible Token contract + + err := interpreterRuntime.ExecuteTransaction( + runtime.Script{ + Source: runtime_utils.DeploymentTransaction( + "FungibleToken", + []byte(realFungibleTokenContractInterface), + ), + }, + runtime.Context{ + Interface: runtimeInterface, + Location: nextTransactionLocation(), + Environment: environment, + }, + ) + require.NoError(b, err) + + // Deploy Flow Token contract + + err = interpreterRuntime.ExecuteTransaction( + runtime.Script{ + Source: []byte(fmt.Sprintf(` + transaction { + prepare(signer: auth(Storage, Capabilities, Contracts) &Account) { + signer.contracts.add(name: "FlowToken", code: "%s".decodeHex(), signer) + } + }`, + hex.EncodeToString([]byte(realFlowContract)), + )), + }, + runtime.Context{ + Interface: runtimeInterface, + Location: nextTransactionLocation(), + Environment: environment, + }, + ) + require.NoError(b, err) + + // Setup both user accounts for Flow Token + + for _, address := range []common.Address{ + senderAddress, + receiverAddress, + } { + + signerAccount = address + + err = interpreterRuntime.ExecuteTransaction( + runtime.Script{ + Source: []byte(realSetupFlowTokenAccountTransaction), + }, + runtime.Context{ + Interface: runtimeInterface, + Location: nextTransactionLocation(), + Environment: environment, + }, + ) + require.NoError(b, err) + } + + // Mint 1000 FLOW to sender + + amount := 100000000000 + mintAmount := cadence.NewInt(amount) + mintAmountValue := interpreter.NewUnmeteredIntValueFromInt64(int64(amount)) + + signerAccount = contractsAddress + + err = interpreterRuntime.ExecuteTransaction( + runtime.Script{ + Source: []byte(realMintFlowTokenTransaction), + Arguments: encodeArgs([]cadence.Value{ + cadence.Address(senderAddress), + mintAmount, + }), + }, + runtime.Context{ + Interface: runtimeInterface, + Location: nextTransactionLocation(), + Environment: environment, + }, + ) + require.NoError(b, err) + + // Benchmark sending tokens from sender to receiver + + sendAmount := cadence.NewInt(1) + + signerAccount = senderAddress + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + + err = interpreterRuntime.ExecuteTransaction( + runtime.Script{ + Source: []byte(realFlowTokenTransferTransaction), + Arguments: encodeArgs([]cadence.Value{ + sendAmount, + cadence.Address(receiverAddress), + }), + }, + runtime.Context{ + Interface: runtimeInterface, + Location: nextTransactionLocation(), + Environment: environment, + }, + ) + require.NoError(b, err) + } + + b.StopTimer() + + // Run validation scripts + + sum := interpreter.NewUnmeteredIntValueFromInt64(0) + + inter := interpreter_utils.NewTestInterpreter(b) + + nextScriptLocation := runtime_utils.NewScriptLocationGenerator() + + for _, address := range []common.Address{ + senderAddress, + receiverAddress, + } { + + result, err := interpreterRuntime.ExecuteScript( + runtime.Script{ + Source: []byte(realFlowTokenBalanceScript), + Arguments: encodeArgs([]cadence.Value{ + cadence.Address(address), + }), + }, + runtime.Context{ + Interface: runtimeInterface, + Location: nextScriptLocation(), + Environment: environment, + }, + ) + require.NoError(b, err) + + value := interpreter.NewUnmeteredIntValueFromBigInt(result.(cadence.Int).Big()) + + require.True(b, bool(value.Less(inter, mintAmountValue, interpreter.EmptyLocationRange))) + + sum = sum.Plus(inter, value, interpreter.EmptyLocationRange).(interpreter.IntValue) + } + + interpreter_utils.RequireValuesEqual(b, nil, mintAmountValue, sum) +} + +func encodeArgs(argValues []cadence.Value) [][]byte { + args := make([][]byte, len(argValues)) + for i, arg := range argValues { + var err error + args[i], err = json.Encode(arg) + if err != nil { + panic(fmt.Errorf("broken test: invalid argument: %w", err)) + } + } + return args +} + +func TestInterpreterImperativeFib(t *testing.T) { + + t.Parallel() + + scriptLocation := runtime_utils.NewScriptLocationGenerator() + + inter, err := parseCheckAndInterpretWithOptions( + t, + imperativeFib, + scriptLocation(), + ParseCheckAndInterpretOptions{}, + ) + require.NoError(t, err) + + var value interpreter.Value = interpreter.NewUnmeteredIntValueFromInt64(7) + + result, err := inter.Invoke("fib", value) + require.NoError(t, err) + require.Equal(t, interpreter.NewUnmeteredIntValueFromInt64(13), result) +} + +func BenchmarkInterpreterImperativeFib(b *testing.B) { + + scriptLocation := runtime_utils.NewScriptLocationGenerator() + + inter, err := parseCheckAndInterpretWithOptions( + b, + imperativeFib, + scriptLocation(), + ParseCheckAndInterpretOptions{}, + ) + require.NoError(b, err) + + var value interpreter.Value = interpreter.NewUnmeteredIntValueFromInt64(14) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, err := inter.Invoke("fib", value) + require.NoError(b, err) + } +} + +func BenchmarkInterpreterNewStruct(b *testing.B) { + + scriptLocation := runtime_utils.NewScriptLocationGenerator() + + inter, err := parseCheckAndInterpretWithOptions( + b, + ` + struct Foo { + var id : Int + + init(_ id: Int) { + self.id = id + } + } + + fun test(count: Int) { + var i = 0 + while i < count { + Foo(i) + i = i + 1 + } + }`, + scriptLocation(), + ParseCheckAndInterpretOptions{}, + ) + require.NoError(b, err) + + value := interpreter.NewIntValueFromInt64(nil, 10) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, err := inter.Invoke("test", value) + require.NoError(b, err) + } +} diff --git a/bbq/vm/test/runtime_test.go b/bbq/vm/test/runtime_test.go new file mode 100644 index 0000000000..49975acf9f --- /dev/null +++ b/bbq/vm/test/runtime_test.go @@ -0,0 +1,453 @@ +/* + * 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 test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/onflow/cadence/bbq/opcode" + "github.com/onflow/cadence/common" + "github.com/onflow/cadence/interpreter" + "github.com/onflow/cadence/test_utils/runtime_utils" + + "github.com/onflow/cadence/bbq" + "github.com/onflow/cadence/bbq/vm" +) + +func TestResourceLossViaSelfRugPull(t *testing.T) { + + // TODO: + t.SkipNow() + + // ---- Deploy FT Contract ----- + + storage := interpreter.NewInMemoryStorage(nil) + + programs := map[common.Location]compiledProgram{} + + contractsAddress := common.MustBytesToAddress([]byte{0x1}) + authorizerAddress := common.MustBytesToAddress([]byte{0x2}) + + // ----- Deploy Contract ----- + + contractCode := ` + access(all) contract Bar { + + access(all) resource Vault { + + // Balance of a user's Vault + // we use unsigned fixed point numbers for balances + // because they can represent decimals and do not allow negative values + access(all) var balance: Int + + init(balance: Int) { + self.balance = balance + } + + access(all) fun withdraw(amount: Int): @Vault { + self.balance = self.balance - amount + return <-create Vault(balance: amount) + } + + access(all) fun deposit(from: @Vault) { + self.balance = self.balance + from.balance + destroy from + } + } + + access(all) fun createEmptyVault(): @Bar.Vault { + return <- create Bar.Vault(balance: 0) + } + + access(all) fun createVault(balance: Int): @Bar.Vault { + return <- create Bar.Vault(balance: balance) + } + } + ` + barLocation := common.NewAddressLocation(nil, contractsAddress, "Bar") + + barProgram := compileCode(t, contractCode, barLocation, programs) + + barVM := vm.NewVM( + barLocation, + barProgram, + &vm.Config{ + Storage: storage, + AccountHandler: &testAccountHandler{}, + }, + ) + + barContractValue, err := barVM.InitializeContract() + require.NoError(t, err) + + // --------- Execute Transaction ------------ + + tx := fmt.Sprintf(` + import Bar from %[1]s + + access(all) contract Foo { + access(all) var rCopy1: @R? + init() { + self.rCopy1 <- nil + var r <- Bar.createVault(balance: 1337) + self.loser(<- r) + } + access(all) resource R { + access(all) var optional: @[Bar.Vault]? + + init() { + self.optional <- [] + } + + access(all) fun rugpullAndAssign(_ callback: fun(): Void, _ victim: @Bar.Vault) { + callback() + // "self" has now been invalidated and accessing "a" for reading would + // trigger a "not initialized" error. However, force-assigning to it succeeds + // and leaves the victim object hanging from an invalidated resource + self.optional <-! [<- victim] + } + } + + access(all) fun loser(_ victim: @Bar.Vault): Void{ + var array: @[R] <- [<- create R()] + let arrRef = &array as auth(Remove) &[R] + fun rugPullCallback(): Void { + // Here we move the R resource from the array to a contract field + // invalidating the "self" during the execution of rugpullAndAssign + Foo.rCopy1 <-! arrRef.removeLast() + } + array[0].rugpullAndAssign(rugPullCallback, <- victim) + destroy array + + var y: @R? <- nil + self.rCopy1 <-> y + destroy y + } + + }`, + contractsAddress.HexWithPrefix(), + ) + + importHandler := func(location common.Location) *bbq.Program[opcode.Instruction] { + switch location { + case barLocation: + return barProgram + default: + assert.FailNow(t, "invalid location") + return nil + } + } + + program := compileCode(t, tx, nil, programs) + + vmConfig := &vm.Config{ + Storage: storage, + ImportHandler: importHandler, + ContractValueHandler: func(_ *vm.Config, location common.Location) *vm.CompositeValue { + switch location { + case barLocation: + return barContractValue + default: + assert.FailNow(t, "invalid location") + return nil + } + }, + } + + txLocation := runtime_utils.NewTransactionLocationGenerator() + + txVM := vm.NewVM(txLocation(), program, vmConfig) + + authorizer := vm.NewAuthAccountReferenceValue(vmConfig, authorizerAddress) + err = txVM.ExecuteTransaction(nil, authorizer) + require.NoError(t, err) + require.Equal(t, 0, txVM.StackSize()) +} + +func TestInterpretResourceReferenceInvalidationOnMove(t *testing.T) { + + t.Parallel() + + //t.Run("stack to account", func(t *testing.T) { + // + // t.Parallel() + // + // code := ` + // resource R { + // access(all) var id: Int + // + // access(all) fun setID(_ id: Int) { + // self.id = id + // } + // + // init() { + // self.id = 1 + // } + // } + // + // fun getRef(_ ref: &R): &R { + // return ref + // } + // + // fun test() { + // let r <-create R() + // let ref = getRef(&r as &R) + // + // // Move the resource into the account + // account.storage.save(<-r, to: /storage/r) + // + // // Update the reference + // ref.setID(2) + // }` + // + // _, err := compileAndInvoke(t, code, "test") + // require.Error(t, err) + // require.ErrorAs(t, err, &interpreter.InvalidatedResourceReferenceError{}) + //}) + // + //t.Run("stack to account readonly", func(t *testing.T) { + // + // t.Parallel() + // + // address := interpreter.NewUnmeteredAddressValueFromBytes([]byte{42}) + // + // inter, _ := testAccountWithErrorHandler(t, address, true, nil, ` + // resource R { + // access(all) var id: Int + // + // init() { + // self.id = 1 + // } + // } + // + // fun test() { + // let r <-create R() + // let ref = &r as &R + // + // // Move the resource into the account + // account.storage.save(<-r, to: /storage/r) + // + // // 'Read' a field from the reference + // let id = ref.id + // }`, sema.Config{}, errorHandler(t)) + // + // _, err := inter.Invoke("test") + // RequireError(t, err) + // require.ErrorAs(t, err, &interpreter.InvalidatedResourceReferenceError{}) + //}) + // + //t.Run("account to stack", func(t *testing.T) { + // + // t.Parallel() + // + // inter := parseCheckAndInterpret(t, ` + // resource R { + // access(all) var id: Int + // + // access(all) fun setID(_ id: Int) { + // self.id = id + // } + // + // init() { + // self.id = 1 + // } + // } + // + // fun test(target: auth(Mutate) &[R]) { + // target.append(<- create R()) + // + // // Take reference while in the account + // let ref = target[0] + // + // // Move the resource out of the account onto the stack + // let movedR <- target.remove(at: 0) + // + // // Update the reference + // ref.setID(2) + // + // destroy movedR + // } + // `) + // + // address := common.Address{0x1} + // + // rType := checker.RequireGlobalType(t, inter.Program.Elaboration, "R").(*sema.CompositeType) + // + // array := interpreter.NewArrayValue( + // inter, + // interpreter.EmptyLocationRange, + // &interpreter.VariableSizedStaticType{ + // Type: interpreter.ConvertSemaToStaticType(nil, rType), + // }, + // address, + // ) + // + // arrayRef := interpreter.NewUnmeteredEphemeralReferenceValue( + // inter, + // interpreter.NewEntitlementSetAuthorization( + // nil, + // func() []common.TypeID { return []common.TypeID{"Mutate"} }, + // 1, + // sema.Conjunction, + // ), + // array, + // &sema.VariableSizedType{ + // Type: rType, + // }, + // interpreter.EmptyLocationRange, + // ) + // + // _, err := inter.Invoke("test", arrayRef) + // RequireError(t, err) + // require.ErrorAs(t, err, &interpreter.InvalidatedResourceReferenceError{}) + //}) + + t.Run("stack to stack", func(t *testing.T) { + + t.Parallel() + + code := ` + resource R { + access(all) var id: Int + + access(all) fun setID(_ id: Int) { + self.id = id + } + + init() { + self.id = 1 + } + } + + fun test() { + let r1 <- create R() + let ref = reference(&r1 as &R) + + // Move the resource onto the same stack + let r2 <- r1 + + // Update the reference + ref.setID(2) + + destroy r2 + } + + fun reference(_ ref: &R): &R { + return ref + }` + + _, err := compileAndInvoke(t, code, "test") + require.Error(t, err) + require.ErrorAs(t, err, &interpreter.InvalidatedResourceReferenceError{}) + }) + + //t.Run("one account to another account", func(t *testing.T) { + // + // t.Parallel() + // + // inter := parseCheckAndInterpret(t, ` + // resource R { + // access(all) var id: Int + // + // access(all) fun setID(_ id: Int) { + // self.id = id + // } + // + // init() { + // self.id = 1 + // } + // } + // + // fun test(target1: auth(Mutate) &[R], target2: auth(Mutate) &[R]) { + // target1.append(<- create R()) + // + // // Take reference while in the account_1 + // let ref = target1[0] + // + // // Move the resource out of the account_1 into the account_2 + // target2.append(<- target1.remove(at: 0)) + // + // // Update the reference + // ref.setID(2) + // } + // `) + // + // rType := checker.RequireGlobalType(t, inter.Program.Elaboration, "R").(*sema.CompositeType) + // + // // Resource array in account 0x01 + // + // array1 := interpreter.NewArrayValue( + // inter, + // interpreter.EmptyLocationRange, + // &interpreter.VariableSizedStaticType{ + // Type: interpreter.ConvertSemaToStaticType(nil, rType), + // }, + // common.Address{0x1}, + // ) + // + // arrayRef1 := interpreter.NewUnmeteredEphemeralReferenceValue( + // inter, + // interpreter.NewEntitlementSetAuthorization( + // nil, + // func() []common.TypeID { return []common.TypeID{"Mutate"} }, + // 1, + // sema.Conjunction, + // ), + // array1, + // &sema.VariableSizedType{ + // Type: rType, + // }, + // interpreter.EmptyLocationRange, + // ) + // + // // Resource array in account 0x02 + // + // array2 := interpreter.NewArrayValue( + // inter, + // interpreter.EmptyLocationRange, + // &interpreter.VariableSizedStaticType{ + // Type: interpreter.ConvertSemaToStaticType(nil, rType), + // }, + // common.Address{0x2}, + // ) + // + // arrayRef2 := interpreter.NewUnmeteredEphemeralReferenceValue( + // inter, + // interpreter.NewEntitlementSetAuthorization( + // nil, + // func() []common.TypeID { return []common.TypeID{"Mutate"} }, + // 1, + // sema.Conjunction, + // ), + // array2, + // &sema.VariableSizedType{ + // Type: rType, + // }, + // interpreter.EmptyLocationRange, + // ) + // + // _, err := inter.Invoke("test", arrayRef1, arrayRef2) + // RequireError(t, err) + // require.ErrorAs(t, err, &interpreter.InvalidatedResourceReferenceError{}) + //}) +} diff --git a/bbq/vm/test/utils.go b/bbq/vm/test/utils.go new file mode 100644 index 0000000000..861e116ba6 --- /dev/null +++ b/bbq/vm/test/utils.go @@ -0,0 +1,490 @@ +/* + * 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 test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/onflow/cadence/ast" + "github.com/onflow/cadence/bbq/opcode" + "github.com/onflow/cadence/common" + "github.com/onflow/cadence/errors" + "github.com/onflow/cadence/interpreter" + "github.com/onflow/cadence/sema" + "github.com/onflow/cadence/stdlib" + "github.com/onflow/cadence/test_utils/runtime_utils" + "github.com/onflow/cadence/test_utils/sema_utils" + + "github.com/onflow/cadence/bbq" + "github.com/onflow/cadence/bbq/commons" + "github.com/onflow/cadence/bbq/compiler" + "github.com/onflow/cadence/bbq/vm" +) + +type testAccountHandler struct { + accountIDs map[common.Address]uint64 + generateAccountID func(address common.Address) (uint64, error) + getAccountBalance func(address common.Address) (uint64, error) + getAccountAvailableBalance func(address common.Address) (uint64, error) + commitStorageTemporarily func(inter *interpreter.Interpreter) error + getStorageUsed func(address common.Address) (uint64, error) + getStorageCapacity func(address common.Address) (uint64, error) + validatePublicKey func(key *stdlib.PublicKey) error + verifySignature func( + signature []byte, + tag string, + signedData []byte, + publicKey []byte, + signatureAlgorithm sema.SignatureAlgorithm, + hashAlgorithm sema.HashAlgorithm, + ) ( + bool, + error, + ) + blsVerifyPOP func(publicKey *stdlib.PublicKey, signature []byte) (bool, error) + hash func(data []byte, tag string, algorithm sema.HashAlgorithm) ([]byte, error) + getAccountKey func(address common.Address, index uint32) (*stdlib.AccountKey, error) + accountKeysCount func(address common.Address) (uint32, error) + emitEvent func( + inter *interpreter.Interpreter, + locationRange interpreter.LocationRange, + eventType *sema.CompositeType, + values []interpreter.Value, + ) + addAccountKey func( + address common.Address, + key *stdlib.PublicKey, + algo sema.HashAlgorithm, + weight int, + ) ( + *stdlib.AccountKey, + error, + ) + revokeAccountKey func(address common.Address, index uint32) (*stdlib.AccountKey, error) + getAccountContractCode func(location common.AddressLocation) ([]byte, error) + parseAndCheckProgram func( + code []byte, + location common.Location, + getAndSetProgram bool, + ) ( + *interpreter.Program, + error, + ) + updateAccountContractCode func(location common.AddressLocation, code []byte) error + recordContractUpdate func(location common.AddressLocation, value *interpreter.CompositeValue) + contractUpdateRecorded func(location common.AddressLocation) bool + interpretContract func( + location common.AddressLocation, + program *interpreter.Program, + name string, + invocation stdlib.DeployedContractConstructorInvocation, + ) ( + *interpreter.CompositeValue, + error, + ) + temporarilyRecordCode func(location common.AddressLocation, code []byte) + removeAccountContractCode func(location common.AddressLocation) error + recordContractRemoval func(location common.AddressLocation) + getAccountContractNames func(address common.Address) ([]string, error) +} + +var _ stdlib.AccountHandler = &testAccountHandler{} + +func (t *testAccountHandler) GenerateAccountID(address common.Address) (uint64, error) { + if t.generateAccountID == nil { + if t.accountIDs == nil { + t.accountIDs = map[common.Address]uint64{} + } + t.accountIDs[address]++ + return t.accountIDs[address], nil + } + return t.generateAccountID(address) +} + +func (t *testAccountHandler) GetAccountBalance(address common.Address) (uint64, error) { + if t.getAccountBalance == nil { + panic(errors.NewUnexpectedError("unexpected call to GetAccountBalance")) + } + return t.getAccountBalance(address) +} + +func (t *testAccountHandler) GetAccountAvailableBalance(address common.Address) (uint64, error) { + if t.getAccountAvailableBalance == nil { + panic(errors.NewUnexpectedError("unexpected call to GetAccountAvailableBalance")) + } + return t.getAccountAvailableBalance(address) +} + +func (t *testAccountHandler) CommitStorageTemporarily(inter *interpreter.Interpreter) error { + if t.commitStorageTemporarily == nil { + panic(errors.NewUnexpectedError("unexpected call to CommitStorageTemporarily")) + } + return t.commitStorageTemporarily(inter) +} + +func (t *testAccountHandler) GetStorageUsed(address common.Address) (uint64, error) { + if t.getStorageUsed == nil { + panic(errors.NewUnexpectedError("unexpected call to GetStorageUsed")) + } + return t.getStorageUsed(address) +} + +func (t *testAccountHandler) GetStorageCapacity(address common.Address) (uint64, error) { + if t.getStorageCapacity == nil { + panic(errors.NewUnexpectedError("unexpected call to GetStorageCapacity")) + } + return t.getStorageCapacity(address) +} + +func (t *testAccountHandler) ValidatePublicKey(key *stdlib.PublicKey) error { + if t.validatePublicKey == nil { + panic(errors.NewUnexpectedError("unexpected call to ValidatePublicKey")) + } + return t.validatePublicKey(key) +} + +func (t *testAccountHandler) VerifySignature( + signature []byte, + tag string, + signedData []byte, + publicKey []byte, + signatureAlgorithm sema.SignatureAlgorithm, + hashAlgorithm sema.HashAlgorithm, +) ( + bool, + error, +) { + if t.verifySignature == nil { + panic(errors.NewUnexpectedError("unexpected call to VerifySignature")) + } + return t.verifySignature( + signature, + tag, + signedData, + publicKey, + signatureAlgorithm, + hashAlgorithm, + ) +} + +func (t *testAccountHandler) BLSVerifyPOP(publicKey *stdlib.PublicKey, signature []byte) (bool, error) { + if t.blsVerifyPOP == nil { + panic(errors.NewUnexpectedError("unexpected call to BLSVerifyPOP")) + } + return t.blsVerifyPOP(publicKey, signature) +} + +func (t *testAccountHandler) Hash(data []byte, tag string, algorithm sema.HashAlgorithm) ([]byte, error) { + if t.hash == nil { + panic(errors.NewUnexpectedError("unexpected call to Hash")) + } + return t.hash(data, tag, algorithm) +} + +func (t *testAccountHandler) GetAccountKey(address common.Address, index uint32) (*stdlib.AccountKey, error) { + if t.getAccountKey == nil { + panic(errors.NewUnexpectedError("unexpected call to GetAccountKey")) + } + return t.getAccountKey(address, index) +} + +func (t *testAccountHandler) AccountKeysCount(address common.Address) (uint32, error) { + if t.accountKeysCount == nil { + panic(errors.NewUnexpectedError("unexpected call to AccountKeysCount")) + } + return t.accountKeysCount(address) +} + +func (t *testAccountHandler) EmitEvent( + inter *interpreter.Interpreter, + locationRange interpreter.LocationRange, + eventType *sema.CompositeType, + values []interpreter.Value, +) { + if t.emitEvent == nil { + panic(errors.NewUnexpectedError("unexpected call to EmitEvent")) + } + t.emitEvent( + inter, + locationRange, + eventType, + values, + ) +} + +func (t *testAccountHandler) AddAccountKey( + address common.Address, + key *stdlib.PublicKey, + algo sema.HashAlgorithm, + weight int, +) ( + *stdlib.AccountKey, + error, +) { + if t.addAccountKey == nil { + panic(errors.NewUnexpectedError("unexpected call to AddAccountKey")) + } + return t.addAccountKey( + address, + key, + algo, + weight, + ) +} + +func (t *testAccountHandler) RevokeAccountKey(address common.Address, index uint32) (*stdlib.AccountKey, error) { + if t.revokeAccountKey == nil { + panic(errors.NewUnexpectedError("unexpected call to RevokeAccountKey")) + } + return t.revokeAccountKey(address, index) +} + +func (t *testAccountHandler) GetAccountContractCode(location common.AddressLocation) ([]byte, error) { + if t.getAccountContractCode == nil { + panic(errors.NewUnexpectedError("unexpected call to GetAccountContractCode")) + } + return t.getAccountContractCode(location) +} + +func (t *testAccountHandler) ParseAndCheckProgram( + code []byte, + location common.Location, + getAndSetProgram bool, +) ( + *interpreter.Program, + error, +) { + if t.parseAndCheckProgram == nil { + panic(errors.NewUnexpectedError("unexpected call to ParseAndCheckProgram")) + } + return t.parseAndCheckProgram(code, location, getAndSetProgram) +} + +func (t *testAccountHandler) UpdateAccountContractCode(location common.AddressLocation, code []byte) error { + if t.updateAccountContractCode == nil { + panic(errors.NewUnexpectedError("unexpected call to UpdateAccountContractCode")) + } + return t.updateAccountContractCode(location, code) +} + +func (t *testAccountHandler) RecordContractUpdate(location common.AddressLocation, value *interpreter.CompositeValue) { + if t.recordContractUpdate == nil { + panic(errors.NewUnexpectedError("unexpected call to RecordContractUpdate")) + } + t.recordContractUpdate(location, value) +} + +func (t *testAccountHandler) ContractUpdateRecorded(location common.AddressLocation) bool { + if t.contractUpdateRecorded == nil { + panic(errors.NewUnexpectedError("unexpected call to ContractUpdateRecorded")) + } + return t.contractUpdateRecorded(location) +} + +func (t *testAccountHandler) InterpretContract( + location common.AddressLocation, + program *interpreter.Program, + name string, + invocation stdlib.DeployedContractConstructorInvocation, +) ( + *interpreter.CompositeValue, + error, +) { + if t.interpretContract == nil { + panic(errors.NewUnexpectedError("unexpected call to InterpretContract")) + } + return t.interpretContract( + location, + program, + name, + invocation, + ) +} + +func (t *testAccountHandler) TemporarilyRecordCode(location common.AddressLocation, code []byte) { + if t.temporarilyRecordCode == nil { + panic(errors.NewUnexpectedError("unexpected call to TemporarilyRecordCode")) + } + t.temporarilyRecordCode(location, code) +} + +func (t *testAccountHandler) RemoveAccountContractCode(location common.AddressLocation) error { + if t.removeAccountContractCode == nil { + panic(errors.NewUnexpectedError("unexpected call to RemoveAccountContractCode")) + } + return t.removeAccountContractCode(location) +} + +func (t *testAccountHandler) RecordContractRemoval(location common.AddressLocation) { + if t.recordContractRemoval == nil { + panic(errors.NewUnexpectedError("unexpected call to RecordContractRemoval")) + } + t.recordContractRemoval(location) +} + +func (t *testAccountHandler) GetAccountContractNames(address common.Address) ([]string, error) { + if t.getAccountContractNames == nil { + panic(errors.NewUnexpectedError("unexpected call to GetAccountContractNames")) + } + return t.getAccountContractNames(address) +} + +func (t *testAccountHandler) StartContractAddition(common.AddressLocation) { + // NO-OP +} + +func (t *testAccountHandler) EndContractAddition(common.AddressLocation) { + // NO-OP +} + +func (t *testAccountHandler) IsContractBeingAdded(common.AddressLocation) bool { + // NO-OP + return false +} + +func singleIdentifierLocationResolver(t testing.TB) func( + identifiers []ast.Identifier, + location common.Location, +) ([]commons.ResolvedLocation, error) { + return func(identifiers []ast.Identifier, location common.Location) ([]commons.ResolvedLocation, error) { + require.Len(t, identifiers, 1) + require.IsType(t, common.AddressLocation{}, location) + + return []commons.ResolvedLocation{ + { + Location: common.AddressLocation{ + Address: location.(common.AddressLocation).Address, + Name: identifiers[0].Identifier, + }, + Identifiers: identifiers, + }, + }, nil + } +} + +func printProgram(name string, program *bbq.Program[opcode.Instruction]) { + printer := bbq.NewInstructionsProgramPrinter() + fmt.Println("===================", name, "===================") + fmt.Println(printer.PrintProgram(program)) +} + +func baseValueActivation(common.Location) *sema.VariableActivation { + // Only need to make the checker happy + activation := sema.NewVariableActivation(sema.BaseValueActivation) + activation.DeclareValue(stdlib.PanicFunction) + activation.DeclareValue(stdlib.NewStandardLibraryStaticFunction( + "getAccount", + stdlib.GetAccountFunctionType, + "", + nil, + )) + return activation +} + +type compiledProgram struct { + *bbq.Program[opcode.Instruction] + *sema.Elaboration +} + +func compileCode( + t testing.TB, + code string, + location common.Location, + programs map[common.Location]compiledProgram, +) *bbq.Program[opcode.Instruction] { + checker := parseAndCheck(t, code, location, programs) + + program := compile(t, checker, programs) + + programs[location] = compiledProgram{ + Program: program, + Elaboration: checker.Elaboration, + } + return program +} + +func parseAndCheck( + t testing.TB, + code string, + location common.Location, + programs map[common.Location]compiledProgram, +) *sema.Checker { + checker, err := sema_utils.ParseAndCheckWithOptions( + t, + code, + sema_utils.ParseAndCheckOptions{ + Location: location, + Config: &sema.Config{ + ImportHandler: func(_ *sema.Checker, location common.Location, _ ast.Range) (sema.Import, error) { + imported, ok := programs[location] + if !ok { + return nil, fmt.Errorf("cannot find contract in location %s", location) + } + + return sema.ElaborationImport{ + Elaboration: imported.Elaboration, + }, nil + }, + LocationHandler: singleIdentifierLocationResolver(t), + BaseValueActivationHandler: baseValueActivation, + }, + }, + ) + require.NoError(t, err) + return checker +} + +func compile( + t testing.TB, + checker *sema.Checker, + programs map[common.Location]compiledProgram, +) *bbq.Program[opcode.Instruction] { + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + comp.Config.LocationHandler = singleIdentifierLocationResolver(t) + comp.Config.ImportHandler = func(location common.Location) *bbq.Program[opcode.Instruction] { + imported, ok := programs[location] + if !ok { + return nil + } + return imported.Program + } + + program := comp.Compile() + return program +} + +func compileAndInvoke(t testing.TB, code string, funcName string) (vm.Value, error) { + location := common.ScriptLocation{0x1} + program := compileCode(t, code, location, map[common.Location]compiledProgram{}) + storage := interpreter.NewInMemoryStorage(nil) + + scriptLocation := runtime_utils.NewScriptLocationGenerator() + + programVM := vm.NewVM( + scriptLocation(), + program, + vm.NewConfig(storage). + WithAccountHandler(&testAccountHandler{}), + ) + + return programVM.Invoke(funcName) +} diff --git a/bbq/vm/test/vm_bench_test.go b/bbq/vm/test/vm_bench_test.go new file mode 100644 index 0000000000..a83bbee2cd --- /dev/null +++ b/bbq/vm/test/vm_bench_test.go @@ -0,0 +1,499 @@ +/* + * 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 test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/onflow/atree" + + "github.com/onflow/cadence/ast" + "github.com/onflow/cadence/bbq" + "github.com/onflow/cadence/bbq/opcode" + "github.com/onflow/cadence/common" + "github.com/onflow/cadence/interpreter" + "github.com/onflow/cadence/sema" + "github.com/onflow/cadence/test_utils/runtime_utils" + . "github.com/onflow/cadence/test_utils/sema_utils" + + "github.com/onflow/cadence/bbq/compiler" + "github.com/onflow/cadence/bbq/vm" +) + +func BenchmarkRecursionFib(b *testing.B) { + + scriptLocation := runtime_utils.NewScriptLocationGenerator() + + checker, err := ParseAndCheck(b, recursiveFib) + require.NoError(b, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + program := comp.Compile() + + vmConfig := &vm.Config{} + vmInstance := vm.NewVM(scriptLocation(), program, vmConfig) + + expected := vm.NewIntValue(377) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + + result, err := vmInstance.Invoke( + "fib", + vm.NewIntValue(14), + ) + require.NoError(b, err) + require.Equal(b, expected, result) + } +} + +func BenchmarkImperativeFib(b *testing.B) { + + scriptLocation := runtime_utils.NewScriptLocationGenerator() + + checker, err := ParseAndCheck(b, imperativeFib) + require.NoError(b, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + program := comp.Compile() + + vmConfig := &vm.Config{} + vmInstance := vm.NewVM(scriptLocation(), program, vmConfig) + + b.ReportAllocs() + b.ResetTimer() + + var value vm.Value = vm.NewIntValue(14) + + for i := 0; i < b.N; i++ { + _, err := vmInstance.Invoke("fib", value) + require.NoError(b, err) + } +} + +func BenchmarkNewStruct(b *testing.B) { + + scriptLocation := runtime_utils.NewScriptLocationGenerator() + + checker, err := ParseAndCheck(b, ` + struct Foo { + var id : Int + + init(_ id: Int) { + self.id = id + } + } + + fun test(count: Int) { + var i = 0 + while i < count { + Foo(i) + i = i + 1 + } + } + `) + require.NoError(b, err) + + value := vm.NewIntValue(10) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + program := comp.Compile() + + vmConfig := &vm.Config{} + vmInstance := vm.NewVM(scriptLocation(), program, vmConfig) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, err := vmInstance.Invoke("test", value) + require.NoError(b, err) + } +} + +func BenchmarkNewResource(b *testing.B) { + + checker, err := ParseAndCheck(b, ` + resource Foo { + var id : Int + + init(_ id: Int) { + self.id = id + } + } + + fun test(count: Int): @Foo { + var i = 0 + var r <- create Foo(0) + while i < count { + i = i + 1 + destroy create Foo(i) + } + return <- r + } + `) + require.NoError(b, err) + + b.ReportAllocs() + b.ResetTimer() + + value := vm.NewIntValue(9) + + scriptLocation := runtime_utils.NewScriptLocationGenerator() + + for i := 0; i < b.N; i++ { + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + program := comp.Compile() + + vmConfig := &vm.Config{} + vmInstance := vm.NewVM(scriptLocation(), program, vmConfig) + _, err := vmInstance.Invoke("test", value) + require.NoError(b, err) + } +} + +func BenchmarkNewStructRaw(b *testing.B) { + + storage := interpreter.NewInMemoryStorage(nil) + vmConfig := &vm.Config{ + Storage: storage, + } + + fieldValue := vm.NewIntValue(7) + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + for j := 0; j < 1; j++ { + structValue := vm.NewCompositeValue( + common.CompositeKindStructure, + interpreter.NewCompositeStaticTypeComputeTypeID( + nil, + common.NewAddressLocation(nil, common.ZeroAddress, "Foo"), + "Foo", + ), + storage.BasicSlabStorage, + ) + structValue.SetMember(vmConfig, "id", fieldValue) + structValue.Transfer(vmConfig, atree.Address{}, false, nil) + } + } +} + +func BenchmarkContractImport(b *testing.B) { + + location := common.NewAddressLocation(nil, common.Address{0x1}, "MyContract") + + importedChecker, err := ParseAndCheckWithOptions(b, + ` + contract MyContract { + var s: String + + fun helloText(): String { + return self.s + } + + init() { + self.s = "contract function of the imported program" + } + + struct Foo { + var id : String + + init(_ id: String) { + self.id = id + } + + fun sayHello(_ id: Int): String { + // return self.id + return MyContract.helloText() + } + } + } + `, + ParseAndCheckOptions{ + Location: location, + }, + ) + require.NoError(b, err) + + importCompiler := compiler.NewInstructionCompiler(importedChecker.Program, importedChecker.Elaboration) + importedProgram := importCompiler.Compile() + + vmInstance := vm.NewVM(location, importedProgram, nil) + importedContractValue, err := vmInstance.InitializeContract() + require.NoError(b, err) + + vmConfig := &vm.Config{ + ImportHandler: func(location common.Location) *bbq.Program[opcode.Instruction] { + return importedProgram + }, + ContractValueHandler: func(vmConfig *vm.Config, location common.Location) *vm.CompositeValue { + return importedContractValue + }, + } + + b.ResetTimer() + b.ReportAllocs() + + value := vm.NewIntValue(7) + + for i := 0; i < b.N; i++ { + checker, err := ParseAndCheckWithOptions(b, ` + import MyContract from 0x01 + + fun test(count: Int): String { + var i = 0 + var r = MyContract.Foo("Hello from Foo!") + while i < count { + i = i + 1 + r = MyContract.Foo("Hello from Foo!") + r.sayHello(1) + } + return r.sayHello(1) + } + `, + ParseAndCheckOptions{ + Config: &sema.Config{ + ImportHandler: func(*sema.Checker, common.Location, ast.Range) (sema.Import, error) { + return sema.ElaborationImport{ + Elaboration: importedChecker.Elaboration, + }, nil + }, + }, + }, + ) + require.NoError(b, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + comp.Config.ImportHandler = func(location common.Location) *bbq.Program[opcode.Instruction] { + return importedProgram + } + program := comp.Compile() + + scriptLocation := runtime_utils.NewScriptLocationGenerator() + + vmInstance := vm.NewVM(scriptLocation(), program, vmConfig) + _, err = vmInstance.Invoke("test", value) + require.NoError(b, err) + } +} + +func BenchmarkMethodCall(b *testing.B) { + + b.Run("interface method call", func(b *testing.B) { + location := common.NewAddressLocation(nil, common.Address{0x1}, "MyContract") + + importedChecker, err := ParseAndCheckWithOptions(b, + ` + contract MyContract { + struct Foo: Greetings { + var id : String + + init(_ id: String) { + self.id = id + } + + fun sayHello(_ id: Int): String { + return self.id + } + } + + struct interface Greetings { + fun sayHello(_ id: Int): String + } + + struct interface SomethingElse { + } + } + `, + ParseAndCheckOptions{ + Location: location, + }, + ) + require.NoError(b, err) + + importCompiler := compiler.NewInstructionCompiler(importedChecker.Program, importedChecker.Elaboration) + importedProgram := importCompiler.Compile() + + vmInstance := vm.NewVM(location, importedProgram, nil) + importedContractValue, err := vmInstance.InitializeContract() + require.NoError(b, err) + + checker, err := ParseAndCheckWithOptions(b, ` + import MyContract from 0x01 + + fun test(count: Int) { + var r: {MyContract.Greetings} = MyContract.Foo("Hello from Foo!") + var i = 0 + while i < count { + i = i + 1 + r.sayHello(1) + } + }`, + + ParseAndCheckOptions{ + Config: &sema.Config{ + ImportHandler: func(*sema.Checker, common.Location, ast.Range) (sema.Import, error) { + return sema.ElaborationImport{ + Elaboration: importedChecker.Elaboration, + }, nil + }, + }, + }, + ) + require.NoError(b, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + comp.Config.ImportHandler = func(location common.Location) *bbq.Program[opcode.Instruction] { + return importedProgram + } + + program := comp.Compile() + + vmConfig := &vm.Config{ + ImportHandler: func(location common.Location) *bbq.Program[opcode.Instruction] { + return importedProgram + }, + ContractValueHandler: func(vmConfig *vm.Config, location common.Location) *vm.CompositeValue { + return importedContractValue + }, + TypeLoader: func(location common.Location, typeID interpreter.TypeID) sema.CompositeKindedType { + elaboration := importedChecker.Elaboration + compositeType := elaboration.CompositeType(typeID) + if compositeType != nil { + return compositeType + } + + return elaboration.InterfaceType(typeID) + }, + } + + scriptLocation := runtime_utils.NewScriptLocationGenerator() + + value := vm.NewIntValue(10) + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + vmInstance = vm.NewVM(scriptLocation(), program, vmConfig) + _, err := vmInstance.Invoke("test", value) + require.NoError(b, err) + } + }) + + b.Run("concrete type method call", func(b *testing.B) { + location := common.NewAddressLocation(nil, common.Address{0x1}, "MyContract") + + importedChecker, err := ParseAndCheckWithOptions(b, + ` + contract MyContract { + struct Foo: Greetings { + var id : String + + init(_ id: String) { + self.id = id + } + + fun sayHello(_ id: Int): String { + return self.id + } + } + + struct interface Greetings { + fun sayHello(_ id: Int): String + } + + struct interface SomethingElse { + } + } + `, + ParseAndCheckOptions{ + Location: location, + }, + ) + require.NoError(b, err) + + importCompiler := compiler.NewInstructionCompiler(importedChecker.Program, importedChecker.Elaboration) + importedProgram := importCompiler.Compile() + + vmInstance := vm.NewVM(location, importedProgram, nil) + importedContractValue, err := vmInstance.InitializeContract() + require.NoError(b, err) + + checker, err := ParseAndCheckWithOptions(b, ` + import MyContract from 0x01 + + fun test(count: Int) { + var r: MyContract.Foo = MyContract.Foo("Hello from Foo!") + var i = 0 + while i < count { + i = i + 1 + r.sayHello(1) + } + }`, + + ParseAndCheckOptions{ + Config: &sema.Config{ + ImportHandler: func(*sema.Checker, common.Location, ast.Range) (sema.Import, error) { + return sema.ElaborationImport{ + Elaboration: importedChecker.Elaboration, + }, nil + }, + }, + }, + ) + require.NoError(b, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + comp.Config.ImportHandler = func(location common.Location) *bbq.Program[opcode.Instruction] { + return importedProgram + } + + program := comp.Compile() + + vmConfig := &vm.Config{ + ImportHandler: func(location common.Location) *bbq.Program[opcode.Instruction] { + return importedProgram + }, + ContractValueHandler: func(vmConfig *vm.Config, location common.Location) *vm.CompositeValue { + return importedContractValue + }, + } + + scriptLocation := runtime_utils.NewScriptLocationGenerator() + + vmInstance = vm.NewVM(scriptLocation(), program, vmConfig) + + value := vm.NewIntValue(10) + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + _, err := vmInstance.Invoke("test", value) + require.NoError(b, err) + } + }) +} diff --git a/bbq/vm/test/vm_test.go b/bbq/vm/test/vm_test.go new file mode 100644 index 0000000000..ab58a8f687 --- /dev/null +++ b/bbq/vm/test/vm_test.go @@ -0,0 +1,2062 @@ +/* + * 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 test + +import ( + "fmt" + "testing" + + "github.com/onflow/cadence/bbq/opcode" + "github.com/onflow/cadence/interpreter" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/onflow/cadence/ast" + "github.com/onflow/cadence/common" + "github.com/onflow/cadence/sema" + "github.com/onflow/cadence/stdlib" + "github.com/onflow/cadence/test_utils/common_utils" + "github.com/onflow/cadence/test_utils/runtime_utils" + . "github.com/onflow/cadence/test_utils/sema_utils" + + "github.com/onflow/cadence/bbq" + "github.com/onflow/cadence/bbq/commons" + "github.com/onflow/cadence/bbq/compiler" + "github.com/onflow/cadence/bbq/vm" +) + +const recursiveFib = ` + fun fib(_ n: Int): Int { + if n < 2 { + return n + } + return fib(n - 1) + fib(n - 2) + } +` + +func scriptLocation() common.Location { + scriptLocation := runtime_utils.NewScriptLocationGenerator() + return scriptLocation() +} + +func TestRecursionFib(t *testing.T) { + + t.Parallel() + + checker, err := ParseAndCheck(t, recursiveFib) + require.NoError(t, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + program := comp.Compile() + + vmConfig := &vm.Config{} + vmInstance := vm.NewVM(scriptLocation(), program, vmConfig) + + result, err := vmInstance.Invoke( + "fib", + vm.NewIntValue(35), + ) + require.NoError(t, err) + require.Equal(t, vm.NewIntValue(9227465), result) + require.Equal(t, 0, vmInstance.StackSize()) +} + +const imperativeFib = ` + fun fib(_ n: Int): Int { + var fib1 = 1 + var fib2 = 1 + var fibonacci = fib1 + var i = 2 + while i < n { + fibonacci = fib1 + fib2 + fib1 = fib2 + fib2 = fibonacci + i = i + 1 + } + return fibonacci + } +` + +func TestImperativeFib(t *testing.T) { + + t.Parallel() + + checker, err := ParseAndCheck(t, imperativeFib) + require.NoError(t, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + program := comp.Compile() + + vmConfig := &vm.Config{} + vmInstance := vm.NewVM(scriptLocation(), program, vmConfig) + + result, err := vmInstance.Invoke( + "fib", + vm.NewIntValue(7), + ) + require.NoError(t, err) + require.Equal(t, vm.NewIntValue(13), result) + require.Equal(t, 0, vmInstance.StackSize()) +} + +func TestBreak(t *testing.T) { + + t.Parallel() + + checker, err := ParseAndCheck(t, ` + fun test(): Int { + var i = 0 + while true { + if i > 3 { + break + } + i = i + 1 + } + return i + } + `) + require.NoError(t, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + program := comp.Compile() + + vmConfig := &vm.Config{} + vmInstance := vm.NewVM(scriptLocation(), program, vmConfig) + + result, err := vmInstance.Invoke("test") + require.NoError(t, err) + + require.Equal(t, vm.NewIntValue(4), result) + require.Equal(t, 0, vmInstance.StackSize()) +} + +func TestContinue(t *testing.T) { + + t.Parallel() + + checker, err := ParseAndCheck(t, ` + fun test(): Int { + var i = 0 + while true { + i = i + 1 + if i < 3 { + continue + } + break + } + return i + } + `) + require.NoError(t, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + program := comp.Compile() + + vmConfig := &vm.Config{} + vmInstance := vm.NewVM(scriptLocation(), program, vmConfig) + + result, err := vmInstance.Invoke("test") + require.NoError(t, err) + + require.Equal(t, vm.NewIntValue(3), result) + require.Equal(t, 0, vmInstance.StackSize()) +} + +func TestNilCoalesce(t *testing.T) { + + t.Parallel() + + t.Run("true", func(t *testing.T) { + t.Parallel() + + checker, err := ParseAndCheck(t, ` + fun test(): Int { + var i: Int? = 2 + var j = i ?? 3 + return j + } + `) + require.NoError(t, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + program := comp.Compile() + + vmConfig := &vm.Config{} + vmInstance := vm.NewVM(scriptLocation(), program, vmConfig) + + result, err := vmInstance.Invoke("test") + require.NoError(t, err) + + require.Equal(t, vm.NewIntValue(2), result) + require.Equal(t, 0, vmInstance.StackSize()) + }) + + t.Run("false", func(t *testing.T) { + t.Parallel() + + checker, err := ParseAndCheck(t, ` + fun test(): Int { + var i: Int? = nil + var j = i ?? 3 + return j + } + `) + require.NoError(t, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + program := comp.Compile() + + vmConfig := &vm.Config{} + vmInstance := vm.NewVM(scriptLocation(), program, vmConfig) + + result, err := vmInstance.Invoke("test") + require.NoError(t, err) + + require.Equal(t, vm.NewIntValue(3), result) + require.Equal(t, 0, vmInstance.StackSize()) + }) +} + +func TestNewStruct(t *testing.T) { + + t.Parallel() + + checker, err := ParseAndCheck(t, ` + struct Foo { + var id : Int + + init(_ id: Int) { + self.id = id + } + } + + fun test(count: Int): Foo { + var i = 0 + var r = Foo(0) + while i < count { + i = i + 1 + r = Foo(i) + r.id = r.id + 2 + } + return r + } + `) + require.NoError(t, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + program := comp.Compile() + + vmConfig := &vm.Config{} + vmInstance := vm.NewVM(scriptLocation(), program, vmConfig) + + result, err := vmInstance.Invoke("test", vm.NewIntValue(10)) + require.NoError(t, err) + require.Equal(t, 0, vmInstance.StackSize()) + + require.IsType(t, &vm.CompositeValue{}, result) + structValue := result.(*vm.CompositeValue) + compositeType := structValue.CompositeType + + require.Equal(t, "Foo", compositeType.QualifiedIdentifier) + require.Equal( + t, + vm.NewIntValue(12), + structValue.GetMember(vmConfig, "id"), + ) +} + +func TestStructMethodCall(t *testing.T) { + + t.Parallel() + + checker, err := ParseAndCheck(t, ` + struct Foo { + var id : String + + init(_ id: String) { + self.id = id + } + + fun sayHello(_ id: Int): String { + return self.id + } + } + + fun test(): String { + var r = Foo("Hello from Foo!") + return r.sayHello(1) + } + `) + require.NoError(t, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + program := comp.Compile() + + vmConfig := &vm.Config{} + vmInstance := vm.NewVM(scriptLocation(), program, vmConfig) + + result, err := vmInstance.Invoke("test") + require.NoError(t, err) + require.Equal(t, 0, vmInstance.StackSize()) + + require.Equal(t, vm.NewStringValue("Hello from Foo!"), result) +} + +func TestImport(t *testing.T) { + + t.Parallel() + + importedChecker, err := ParseAndCheckWithOptions(t, + ` + fun helloText(): String { + return "global function of the imported program" + } + + struct Foo { + var id : String + + init(_ id: String) { + self.id = id + } + + fun sayHello(_ id: Int): String { + self.id + return helloText() + } + } + + `, + ParseAndCheckOptions{ + Location: common_utils.ImportedLocation, + }, + ) + require.NoError(t, err) + + subComp := compiler.NewInstructionCompiler(importedChecker.Program, importedChecker.Elaboration) + importedProgram := subComp.Compile() + + checker, err := ParseAndCheckWithOptions(t, ` + import Foo from 0x01 + + fun test(): String { + var r = Foo("Hello from Foo!") + return r.sayHello(1) + } + `, + ParseAndCheckOptions{ + Config: &sema.Config{ + ImportHandler: func(*sema.Checker, common.Location, ast.Range) (sema.Import, error) { + return sema.ElaborationImport{ + Elaboration: importedChecker.Elaboration, + }, nil + }, + }, + }, + ) + require.NoError(t, err) + + importCompiler := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + importCompiler.Config.ImportHandler = func(location common.Location) *bbq.Program[opcode.Instruction] { + return importedProgram + } + + program := importCompiler.Compile() + + vmConfig := &vm.Config{ + ImportHandler: func(location common.Location) *bbq.Program[opcode.Instruction] { + return importedProgram + }, + } + + vmInstance := vm.NewVM(scriptLocation(), program, vmConfig) + + result, err := vmInstance.Invoke("test") + require.NoError(t, err) + require.Equal(t, 0, vmInstance.StackSize()) + + require.Equal(t, vm.NewStringValue("global function of the imported program"), result) +} + +func TestContractImport(t *testing.T) { + + t.Parallel() + + t.Run("nested type def", func(t *testing.T) { + + importLocation := common.NewAddressLocation(nil, common.Address{0x1}, "MyContract") + + importedChecker, err := ParseAndCheckWithOptions(t, + ` + contract MyContract { + + fun helloText(): String { + return "global function of the imported program" + } + + init() {} + + struct Foo { + var id : String + + init(_ id: String) { + self.id = id + } + + fun sayHello(_ id: Int): String { + self.id + return MyContract.helloText() + } + } + } + `, + ParseAndCheckOptions{ + Location: importLocation, + }, + ) + require.NoError(t, err) + + importCompiler := compiler.NewInstructionCompiler(importedChecker.Program, importedChecker.Elaboration) + importedProgram := importCompiler.Compile() + + vmInstance := vm.NewVM(importLocation, importedProgram, nil) + importedContractValue, err := vmInstance.InitializeContract() + require.NoError(t, err) + + checker, err := ParseAndCheckWithOptions(t, ` + import MyContract from 0x01 + + fun test(): String { + var r = MyContract.Foo("Hello from Foo!") + return r.sayHello(1) + } + `, + ParseAndCheckOptions{ + Config: &sema.Config{ + ImportHandler: func(*sema.Checker, common.Location, ast.Range) (sema.Import, error) { + return sema.ElaborationImport{ + Elaboration: importedChecker.Elaboration, + }, nil + }, + }, + }, + ) + require.NoError(t, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + comp.Config.ImportHandler = func(location common.Location) *bbq.Program[opcode.Instruction] { + return importedProgram + } + + program := comp.Compile() + + vmConfig := &vm.Config{ + ImportHandler: func(location common.Location) *bbq.Program[opcode.Instruction] { + return importedProgram + }, + ContractValueHandler: func(*vm.Config, common.Location) *vm.CompositeValue { + return importedContractValue + }, + } + + vmInstance = vm.NewVM(scriptLocation(), program, vmConfig) + + result, err := vmInstance.Invoke("test") + require.NoError(t, err) + require.Equal(t, 0, vmInstance.StackSize()) + + require.Equal(t, vm.NewStringValue("global function of the imported program"), result) + }) + + t.Run("contract function", func(t *testing.T) { + importLocation := common.NewAddressLocation(nil, common.Address{0x1}, "MyContract") + + importedChecker, err := ParseAndCheckWithOptions(t, + ` + contract MyContract { + + var s: String + + fun helloText(): String { + return self.s + } + + init() { + self.s = "contract function of the imported program" + } + } + `, + ParseAndCheckOptions{ + Location: importLocation, + }, + ) + require.NoError(t, err) + + importCompiler := compiler.NewInstructionCompiler(importedChecker.Program, importedChecker.Elaboration) + importedProgram := importCompiler.Compile() + + vmInstance := vm.NewVM(importLocation, importedProgram, nil) + importedContractValue, err := vmInstance.InitializeContract() + require.NoError(t, err) + + checker, err := ParseAndCheckWithOptions(t, ` + import MyContract from 0x01 + + fun test(): String { + return MyContract.helloText() + } + `, + ParseAndCheckOptions{ + Config: &sema.Config{ + ImportHandler: func(*sema.Checker, common.Location, ast.Range) (sema.Import, error) { + return sema.ElaborationImport{ + Elaboration: importedChecker.Elaboration, + }, nil + }, + }, + }, + ) + require.NoError(t, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + comp.Config.ImportHandler = func(location common.Location) *bbq.Program[opcode.Instruction] { + return importedProgram + } + + program := comp.Compile() + + vmConfig := &vm.Config{ + ImportHandler: func(location common.Location) *bbq.Program[opcode.Instruction] { + return importedProgram + }, + ContractValueHandler: func(*vm.Config, common.Location) *vm.CompositeValue { + return importedContractValue + }, + } + + vmInstance = vm.NewVM(scriptLocation(), program, vmConfig) + + result, err := vmInstance.Invoke("test") + require.NoError(t, err) + require.Equal(t, 0, vmInstance.StackSize()) + + require.Equal(t, vm.NewStringValue("contract function of the imported program"), result) + }) + + t.Run("nested imports", func(t *testing.T) { + + // Initialize Foo + + fooLocation := common.NewAddressLocation( + nil, + common.Address{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1}, + "Foo", + ) + + fooChecker, err := ParseAndCheckWithOptions(t, + ` + contract Foo { + var s: String + init() { + self.s = "Hello from Foo!" + } + fun sayHello(): String { + return self.s + } + }`, + ParseAndCheckOptions{ + Location: fooLocation, + }, + ) + require.NoError(t, err) + + fooCompiler := compiler.NewInstructionCompiler(fooChecker.Program, fooChecker.Elaboration) + fooProgram := fooCompiler.Compile() + + vmInstance := vm.NewVM(fooLocation, fooProgram, nil) + fooContractValue, err := vmInstance.InitializeContract() + require.NoError(t, err) + + // Initialize Bar + + barLocation := common.NewAddressLocation( + nil, + common.Address{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2}, + "Bar", + ) + + barChecker, err := ParseAndCheckWithOptions(t, ` + import Foo from 0x01 + + contract Bar { + init() {} + fun sayHello(): String { + return Foo.sayHello() + } + }`, + ParseAndCheckOptions{ + Location: barLocation, + Config: &sema.Config{ + ImportHandler: func(_ *sema.Checker, location common.Location, _ ast.Range) (sema.Import, error) { + require.Equal(t, fooLocation, location) + return sema.ElaborationImport{ + Elaboration: fooChecker.Elaboration, + }, nil + }, + LocationHandler: singleIdentifierLocationResolver(t), + }, + }, + ) + require.NoError(t, err) + + barCompiler := compiler.NewInstructionCompiler(barChecker.Program, barChecker.Elaboration) + barCompiler.Config.LocationHandler = singleIdentifierLocationResolver(t) + barCompiler.Config.ImportHandler = func(location common.Location) *bbq.Program[opcode.Instruction] { + require.Equal(t, fooLocation, location) + return fooProgram + } + + barProgram := barCompiler.Compile() + + vmConfig := &vm.Config{ + ImportHandler: func(location common.Location) *bbq.Program[opcode.Instruction] { + require.Equal(t, fooLocation, location) + return fooProgram + }, + ContractValueHandler: func(_ *vm.Config, location common.Location) *vm.CompositeValue { + require.Equal(t, fooLocation, location) + return fooContractValue + }, + } + + vmInstance = vm.NewVM(barLocation, barProgram, vmConfig) + barContractValue, err := vmInstance.InitializeContract() + require.NoError(t, err) + + // Compile and run main program + + checker, err := ParseAndCheckWithOptions(t, ` + import Bar from 0x02 + + fun test(): String { + return Bar.sayHello() + }`, + ParseAndCheckOptions{ + Config: &sema.Config{ + ImportHandler: func(_ *sema.Checker, location common.Location, _ ast.Range) (sema.Import, error) { + require.IsType(t, common.AddressLocation{}, location) + addressLocation := location.(common.AddressLocation) + var elaboration *sema.Elaboration + switch addressLocation.Address { + case fooLocation.Address: + elaboration = fooChecker.Elaboration + case barLocation.Address: + elaboration = barChecker.Elaboration + default: + assert.FailNow(t, "invalid location") + } + + return sema.ElaborationImport{ + Elaboration: elaboration, + }, nil + }, + LocationHandler: singleIdentifierLocationResolver(t), + }, + }, + ) + require.NoError(t, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + comp.Config.LocationHandler = singleIdentifierLocationResolver(t) + comp.Config.ImportHandler = func(location common.Location) *bbq.Program[opcode.Instruction] { + switch location { + case fooLocation: + return fooProgram + case barLocation: + return barProgram + default: + assert.FailNow(t, "invalid location") + return nil + } + } + + program := comp.Compile() + + vmConfig = &vm.Config{ + ImportHandler: func(location common.Location) *bbq.Program[opcode.Instruction] { + switch location { + case fooLocation: + return fooProgram + case barLocation: + return barProgram + default: + assert.FailNow(t, "invalid location") + return nil + } + }, + ContractValueHandler: func(_ *vm.Config, location common.Location) *vm.CompositeValue { + switch location { + case fooLocation: + return fooContractValue + case barLocation: + return barContractValue + default: + assert.FailNow(t, "invalid location") + return nil + } + }, + } + + vmInstance = vm.NewVM(scriptLocation(), program, vmConfig) + + result, err := vmInstance.Invoke("test") + require.NoError(t, err) + require.Equal(t, 0, vmInstance.StackSize()) + + require.Equal(t, vm.NewStringValue("Hello from Foo!"), result) + }) + + t.Run("contract interface", func(t *testing.T) { + + // Initialize Foo + + fooLocation := common.NewAddressLocation( + nil, + common.Address{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1}, + "Foo", + ) + + fooChecker, err := ParseAndCheckWithOptions(t, + ` + contract interface Foo { + fun withdraw(_ amount: Int): String { + pre { + amount < 100: "Withdraw limit exceeds" + } + } + }`, + ParseAndCheckOptions{ + Location: fooLocation, + }, + ) + require.NoError(t, err) + + fooCompiler := compiler.NewInstructionCompiler(fooChecker.Program, fooChecker.Elaboration) + fooProgram := fooCompiler.Compile() + + //vmInstance := NewVM(fooProgram, nil) + //fooContractValue, err := vmInstance.InitializeContract() + //require.NoError(t, err) + + // Initialize Bar + + barLocation := common.NewAddressLocation( + nil, + common.Address{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2}, + "Bar", + ) + + barChecker, err := ParseAndCheckWithOptions(t, ` + import Foo from 0x01 + + contract Bar: Foo { + init() {} + fun withdraw(_ amount: Int): String { + return "Successfully withdrew" + } + }`, + ParseAndCheckOptions{ + Location: barLocation, + Config: &sema.Config{ + ImportHandler: func(_ *sema.Checker, location common.Location, _ ast.Range) (sema.Import, error) { + require.Equal(t, fooLocation, location) + return sema.ElaborationImport{ + Elaboration: fooChecker.Elaboration, + }, nil + }, + LocationHandler: singleIdentifierLocationResolver(t), + }, + }, + ) + require.NoError(t, err) + + barCompiler := compiler.NewInstructionCompiler(barChecker.Program, barChecker.Elaboration) + barCompiler.Config.LocationHandler = singleIdentifierLocationResolver(t) + barCompiler.Config.ImportHandler = func(location common.Location) *bbq.Program[opcode.Instruction] { + require.Equal(t, fooLocation, location) + return fooProgram + } + + barProgram := barCompiler.Compile() + + vmConfig := &vm.Config{ + ImportHandler: func(location common.Location) *bbq.Program[opcode.Instruction] { + require.Equal(t, fooLocation, location) + return fooProgram + }, + //ContractValueHandler: func(_ *Config, location common.Location) *CompositeValue { + // require.Equal(t, fooLocation, location) + // return fooContractValue + //}, + } + + vmInstance := vm.NewVM(barLocation, barProgram, vmConfig) + barContractValue, err := vmInstance.InitializeContract() + require.NoError(t, err) + + // Compile and run main program + + checker, err := ParseAndCheckWithOptions(t, ` + import Bar from 0x02 + + fun test(): String { + return Bar.withdraw(150) + }`, + ParseAndCheckOptions{ + Config: &sema.Config{ + ImportHandler: func(_ *sema.Checker, location common.Location, _ ast.Range) (sema.Import, error) { + require.IsType(t, common.AddressLocation{}, location) + addressLocation := location.(common.AddressLocation) + var elaboration *sema.Elaboration + switch addressLocation.Address { + case fooLocation.Address: + elaboration = fooChecker.Elaboration + case barLocation.Address: + elaboration = barChecker.Elaboration + default: + assert.FailNow(t, "invalid location") + } + + return sema.ElaborationImport{ + Elaboration: elaboration, + }, nil + }, + LocationHandler: singleIdentifierLocationResolver(t), + }, + }, + ) + require.NoError(t, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + comp.Config.LocationHandler = singleIdentifierLocationResolver(t) + comp.Config.ImportHandler = func(location common.Location) *bbq.Program[opcode.Instruction] { + switch location { + case fooLocation: + return fooProgram + case barLocation: + return barProgram + default: + assert.FailNow(t, "invalid location") + return nil + } + } + + program := comp.Compile() + + vmConfig = &vm.Config{ + ImportHandler: func(location common.Location) *bbq.Program[opcode.Instruction] { + switch location { + case fooLocation: + return fooProgram + case barLocation: + return barProgram + default: + assert.FailNow(t, "invalid location") + return nil + } + }, + ContractValueHandler: func(_ *vm.Config, location common.Location) *vm.CompositeValue { + switch location { + //case fooLocation: + // return fooContractValue + case barLocation: + return barContractValue + default: + assert.FailNow(t, fmt.Sprintf("invalid location %s", location)) + return nil + } + }, + } + + vmInstance = vm.NewVM(scriptLocation(), program, vmConfig) + + result, err := vmInstance.Invoke("test") + require.NoError(t, err) + require.Equal(t, 0, vmInstance.StackSize()) + + require.Equal(t, vm.NewStringValue("Successfully withdrew"), result) + }) +} + +func TestInitializeContract(t *testing.T) { + + checker, err := ParseAndCheckWithOptions(t, + ` + contract MyContract { + var status : String + init() { + self.status = "PENDING" + } + } + `, + ParseAndCheckOptions{ + Location: common.NewAddressLocation(nil, common.Address{0x1}, "MyContract"), + }, + ) + require.NoError(t, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + program := comp.Compile() + + vmConfig := &vm.Config{} + vmInstance := vm.NewVM(scriptLocation(), program, vmConfig) + contractValue, err := vmInstance.InitializeContract() + require.NoError(t, err) + + fieldValue := contractValue.GetMember(vmConfig, "status") + assert.Equal(t, vm.NewStringValue("PENDING"), fieldValue) +} + +func TestContractAccessDuringInit(t *testing.T) { + + t.Parallel() + + t.Run("using contract name", func(t *testing.T) { + t.Parallel() + + checker, err := ParseAndCheckWithOptions(t, ` + contract MyContract { + var status : String + + access(all) fun getInitialStatus(): String { + return "PENDING" + } + + init() { + self.status = MyContract.getInitialStatus() + } + }`, + ParseAndCheckOptions{ + Location: common.NewAddressLocation(nil, common.Address{0x1}, "MyContract"), + }, + ) + require.NoError(t, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + program := comp.Compile() + + vmConfig := &vm.Config{} + vmInstance := vm.NewVM(scriptLocation(), program, vmConfig) + contractValue, err := vmInstance.InitializeContract() + require.NoError(t, err) + + fieldValue := contractValue.GetMember(vmConfig, "status") + assert.Equal(t, vm.NewStringValue("PENDING"), fieldValue) + }) + + t.Run("using self", func(t *testing.T) { + t.Parallel() + + checker, err := ParseAndCheckWithOptions(t, ` + contract MyContract { + var status : String + + access(all) fun getInitialStatus(): String { + return "PENDING" + } + + init() { + self.status = self.getInitialStatus() + } + }`, + ParseAndCheckOptions{ + Location: common.NewAddressLocation(nil, common.Address{0x1}, "MyContract"), + }, + ) + require.NoError(t, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + program := comp.Compile() + + vmConfig := &vm.Config{} + vmInstance := vm.NewVM(scriptLocation(), program, vmConfig) + contractValue, err := vmInstance.InitializeContract() + require.NoError(t, err) + + fieldValue := contractValue.GetMember(vmConfig, "status") + assert.Equal(t, vm.NewStringValue("PENDING"), fieldValue) + }) +} + +func TestFunctionOrder(t *testing.T) { + + t.Parallel() + + t.Run("top level", func(t *testing.T) { + t.Parallel() + + checker, err := ParseAndCheck(t, ` + fun foo(): Int { + return 2 + } + + fun test(): Int { + return foo() + bar() + } + + fun bar(): Int { + return 3 + }`) + require.NoError(t, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + program := comp.Compile() + + vmConfig := &vm.Config{} + vmInstance := vm.NewVM(scriptLocation(), program, vmConfig) + + result, err := vmInstance.Invoke("test") + require.NoError(t, err) + require.Equal(t, 0, vmInstance.StackSize()) + + require.Equal(t, vm.NewIntValue(5), result) + }) + + t.Run("nested", func(t *testing.T) { + t.Parallel() + + code := ` + contract MyContract { + + fun helloText(): String { + return "global function of the imported program" + } + + init() {} + + fun initializeFoo() { + MyContract.Foo("one") + } + + struct Foo { + var id : String + + init(_ id: String) { + self.id = id + } + + fun sayHello(_ id: Int): String { + self.id + return MyContract.helloText() + } + } + + fun initializeFooAgain() { + MyContract.Foo("two") + } + }` + + checker, err := ParseAndCheckWithOptions( + t, + code, + ParseAndCheckOptions{ + Location: common.NewAddressLocation(nil, common.Address{0x1}, "MyContract"), + }, + ) + require.NoError(t, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + program := comp.Compile() + + vmConfig := &vm.Config{} + vmInstance := vm.NewVM(scriptLocation(), program, vmConfig) + + result, err := vmInstance.Invoke("init") + require.NoError(t, err) + require.Equal(t, 0, vmInstance.StackSize()) + + require.IsType(t, &vm.CompositeValue{}, result) + }) +} + +func TestContractField(t *testing.T) { + + t.Parallel() + + t.Run("get", func(t *testing.T) { + importLocation := common.NewAddressLocation(nil, common.Address{0x1}, "MyContract") + + importedChecker, err := ParseAndCheckWithOptions(t, + ` + contract MyContract { + var status : String + + init() { + self.status = "PENDING" + } + } + `, + ParseAndCheckOptions{ + Location: common.NewAddressLocation(nil, common.Address{0x1}, "MyContract"), + }, + ) + require.NoError(t, err) + + importCompiler := compiler.NewInstructionCompiler(importedChecker.Program, importedChecker.Elaboration) + importedProgram := importCompiler.Compile() + + vmInstance := vm.NewVM(importLocation, importedProgram, nil) + importedContractValue, err := vmInstance.InitializeContract() + require.NoError(t, err) + + checker, err := ParseAndCheckWithOptions(t, ` + import MyContract from 0x01 + + fun test(): String { + return MyContract.status + } + `, + ParseAndCheckOptions{ + Config: &sema.Config{ + ImportHandler: func(*sema.Checker, common.Location, ast.Range) (sema.Import, error) { + return sema.ElaborationImport{ + Elaboration: importedChecker.Elaboration, + }, nil + }, + }, + }, + ) + require.NoError(t, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + comp.Config.ImportHandler = func(location common.Location) *bbq.Program[opcode.Instruction] { + return importedProgram + } + + program := comp.Compile() + + vmConfig := &vm.Config{ + ImportHandler: func(location common.Location) *bbq.Program[opcode.Instruction] { + return importedProgram + }, + ContractValueHandler: func(vmConfig *vm.Config, location common.Location) *vm.CompositeValue { + return importedContractValue + }, + } + + vmInstance = vm.NewVM(scriptLocation(), program, vmConfig) + result, err := vmInstance.Invoke("test") + require.NoError(t, err) + require.Equal(t, 0, vmInstance.StackSize()) + + require.Equal(t, vm.NewStringValue("PENDING"), result) + }) + + t.Run("set", func(t *testing.T) { + importLocation := common.NewAddressLocation(nil, common.Address{0x1}, "MyContract") + + importedChecker, err := ParseAndCheckWithOptions(t, + ` + contract MyContract { + var status : String + + init() { + self.status = "PENDING" + } + } + `, + ParseAndCheckOptions{ + Location: common.NewAddressLocation(nil, common.Address{0x1}, "MyContract"), + }, + ) + require.NoError(t, err) + + importCompiler := compiler.NewInstructionCompiler(importedChecker.Program, importedChecker.Elaboration) + importedProgram := importCompiler.Compile() + + vmInstance := vm.NewVM(importLocation, importedProgram, nil) + importedContractValue, err := vmInstance.InitializeContract() + require.NoError(t, err) + + checker, err := ParseAndCheckWithOptions(t, ` + import MyContract from 0x01 + + fun test(): String { + MyContract.status = "UPDATED" + return MyContract.status + } + `, + ParseAndCheckOptions{ + Config: &sema.Config{ + ImportHandler: func(*sema.Checker, common.Location, ast.Range) (sema.Import, error) { + return sema.ElaborationImport{ + Elaboration: importedChecker.Elaboration, + }, nil + }, + }, + }, + ) + require.NoError(t, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + comp.Config.ImportHandler = func(location common.Location) *bbq.Program[opcode.Instruction] { + return importedProgram + } + + program := comp.Compile() + + vmConfig := &vm.Config{ + ImportHandler: func(location common.Location) *bbq.Program[opcode.Instruction] { + return importedProgram + }, + ContractValueHandler: func(vmConfig *vm.Config, location common.Location) *vm.CompositeValue { + return importedContractValue + }, + } + + vmInstance = vm.NewVM(scriptLocation(), program, vmConfig) + + result, err := vmInstance.Invoke("test") + require.NoError(t, err) + require.Equal(t, 0, vmInstance.StackSize()) + + require.Equal(t, vm.NewStringValue("UPDATED"), result) + + fieldValue := importedContractValue.GetMember(vmConfig, "status") + assert.Equal(t, vm.NewStringValue("UPDATED"), fieldValue) + }) +} + +func TestNativeFunctions(t *testing.T) { + + t.Parallel() + + t.Run("static function", func(t *testing.T) { + + logFunction := stdlib.NewStandardLibraryStaticFunction( + "log", + &sema.FunctionType{ + Parameters: []sema.Parameter{ + { + Label: sema.ArgumentLabelNotRequired, + Identifier: "value", + TypeAnnotation: sema.NewTypeAnnotation(sema.AnyStructType), + }, + }, + ReturnTypeAnnotation: sema.NewTypeAnnotation( + sema.VoidType, + ), + }, + ``, + nil, + ) + + baseValueActivation := sema.NewVariableActivation(sema.BaseValueActivation) + baseValueActivation.DeclareValue(logFunction) + + checker, err := ParseAndCheckWithOptions(t, ` + fun test() { + log("Hello, World!") + }`, + ParseAndCheckOptions{ + Config: &sema.Config{ + BaseValueActivationHandler: func(common.Location) *sema.VariableActivation { + return baseValueActivation + }, + }, + }, + ) + require.NoError(t, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + program := comp.Compile() + + vmConfig := &vm.Config{} + vmInstance := vm.NewVM(scriptLocation(), program, vmConfig) + + _, err = vmInstance.Invoke("test") + require.NoError(t, err) + require.Equal(t, 0, vmInstance.StackSize()) + }) + + t.Run("bound function", func(t *testing.T) { + checker, err := ParseAndCheck(t, ` + fun test(): String { + return "Hello".concat(", World!") + }`, + ) + require.NoError(t, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + program := comp.Compile() + + vmConfig := &vm.Config{} + vmInstance := vm.NewVM(scriptLocation(), program, vmConfig) + + result, err := vmInstance.Invoke("test") + require.NoError(t, err) + require.Equal(t, 0, vmInstance.StackSize()) + + require.Equal(t, vm.NewStringValue("Hello, World!"), result) + }) +} + +func TestTransaction(t *testing.T) { + + t.Parallel() + + t.Run("simple", func(t *testing.T) { + + checker, err := ParseAndCheck(t, ` + transaction { + var a: String + prepare() { + self.a = "Hello!" + } + execute { + self.a = "Hello again!" + } + }`, + ) + require.NoError(t, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + program := comp.Compile() + + vmConfig := &vm.Config{} + vmInstance := vm.NewVM(scriptLocation(), program, vmConfig) + + err = vmInstance.ExecuteTransaction(nil) + require.NoError(t, err) + require.Equal(t, 0, vmInstance.StackSize()) + + // Rerun the same again using internal functions, to get the access to the transaction value. + + transaction, err := vmInstance.Invoke(commons.TransactionWrapperCompositeName) + require.NoError(t, err) + + require.IsType(t, &vm.CompositeValue{}, transaction) + compositeValue := transaction.(*vm.CompositeValue) + + // At the beginning, 'a' is uninitialized + assert.Nil(t, compositeValue.GetMember(vmConfig, "a")) + + // Invoke 'prepare' + _, err = vmInstance.Invoke(commons.TransactionPrepareFunctionName, transaction) + require.NoError(t, err) + + // Once 'prepare' is called, 'a' is initialized to "Hello!" + assert.Equal(t, vm.NewStringValue("Hello!"), compositeValue.GetMember(vmConfig, "a")) + + // Invoke 'execute' + _, err = vmInstance.Invoke(commons.TransactionExecuteFunctionName, transaction) + require.NoError(t, err) + + // Once 'execute' is called, 'a' is initialized to "Hello, again!" + assert.Equal(t, vm.NewStringValue("Hello again!"), compositeValue.GetMember(vmConfig, "a")) + }) + + t.Run("with params", func(t *testing.T) { + + checker, err := ParseAndCheck(t, ` + transaction(param1: String, param2: String) { + var a: String + prepare() { + self.a = param1 + } + execute { + self.a = param2 + } + }`, + ) + require.NoError(t, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + program := comp.Compile() + + vmConfig := &vm.Config{} + vmInstance := vm.NewVM(scriptLocation(), program, vmConfig) + + args := []vm.Value{ + vm.NewStringValue("Hello!"), + vm.NewStringValue("Hello again!"), + } + + err = vmInstance.ExecuteTransaction(args) + require.NoError(t, err) + require.Equal(t, 0, vmInstance.StackSize()) + + // Rerun the same again using internal functions, to get the access to the transaction value. + + transaction, err := vmInstance.Invoke(commons.TransactionWrapperCompositeName) + require.NoError(t, err) + + require.IsType(t, &vm.CompositeValue{}, transaction) + compositeValue := transaction.(*vm.CompositeValue) + + // At the beginning, 'a' is uninitialized + assert.Nil(t, compositeValue.GetMember(vmConfig, "a")) + + // Invoke 'prepare' + _, err = vmInstance.Invoke(commons.TransactionPrepareFunctionName, transaction) + require.NoError(t, err) + + // Once 'prepare' is called, 'a' is initialized to "Hello!" + assert.Equal(t, vm.NewStringValue("Hello!"), compositeValue.GetMember(vmConfig, "a")) + + // Invoke 'execute' + _, err = vmInstance.Invoke(commons.TransactionExecuteFunctionName, transaction) + require.NoError(t, err) + + // Once 'execute' is called, 'a' is initialized to "Hello, again!" + assert.Equal(t, vm.NewStringValue("Hello again!"), compositeValue.GetMember(vmConfig, "a")) + }) +} + +func TestInterfaceMethodCall(t *testing.T) { + + t.Parallel() + + t.Run("impl in same program", func(t *testing.T) { + + t.Parallel() + + contractLocation := common.NewAddressLocation( + nil, + common.Address{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1}, + "MyContract", + ) + + importedChecker, err := ParseAndCheckWithOptions(t, + ` + contract MyContract { + struct Foo: Greetings { + var id : String + + init(_ id: String) { + self.id = id + } + + fun sayHello(_ id: Int): String { + return self.id + } + } + + struct interface Greetings { + fun sayHello(_ id: Int): String + } + + struct interface SomethingElse { + } + } + `, + ParseAndCheckOptions{ + Location: contractLocation, + }, + ) + require.NoError(t, err) + + importCompiler := compiler.NewInstructionCompiler(importedChecker.Program, importedChecker.Elaboration) + importedProgram := importCompiler.Compile() + + vmInstance := vm.NewVM(contractLocation, importedProgram, nil) + importedContractValue, err := vmInstance.InitializeContract() + require.NoError(t, err) + + checker, err := ParseAndCheckWithOptions(t, ` + import MyContract from 0x01 + + fun test(): String { + var r: {MyContract.Greetings} = MyContract.Foo("Hello from Foo!") + // first call must link + r.sayHello(1) + + // second call should pick from the cache + return r.sayHello(1) + }`, + + ParseAndCheckOptions{ + Config: &sema.Config{ + ImportHandler: func(*sema.Checker, common.Location, ast.Range) (sema.Import, error) { + return sema.ElaborationImport{ + Elaboration: importedChecker.Elaboration, + }, nil + }, + LocationHandler: singleIdentifierLocationResolver(t), + }, + }, + ) + require.NoError(t, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + comp.Config.LocationHandler = singleIdentifierLocationResolver(t) + comp.Config.ImportHandler = func(location common.Location) *bbq.Program[opcode.Instruction] { + return importedProgram + } + + program := comp.Compile() + + vmConfig := &vm.Config{ + ImportHandler: func(location common.Location) *bbq.Program[opcode.Instruction] { + return importedProgram + }, + ContractValueHandler: func(vmConfig *vm.Config, location common.Location) *vm.CompositeValue { + return importedContractValue + }, + TypeLoader: func(location common.Location, typeID interpreter.TypeID) sema.CompositeKindedType { + elaboration := importedChecker.Elaboration + compositeType := elaboration.CompositeType(typeID) + if compositeType != nil { + return compositeType + } + + return elaboration.InterfaceType(typeID) + }, + } + + vmInstance = vm.NewVM(scriptLocation(), program, vmConfig) + result, err := vmInstance.Invoke("test") + require.NoError(t, err) + require.Equal(t, 0, vmInstance.StackSize()) + + require.Equal(t, vm.NewStringValue("Hello from Foo!"), result) + }) + + t.Run("impl in different program", func(t *testing.T) { + + t.Parallel() + + // Define the interface in `Foo` + + fooLocation := common.NewAddressLocation( + nil, + common.Address{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1}, + "Foo", + ) + + fooChecker, err := ParseAndCheckWithOptions(t, + ` + contract Foo { + struct interface Greetings { + fun sayHello(): String + } + }`, + ParseAndCheckOptions{ + Location: fooLocation, + }, + ) + require.NoError(t, err) + + interfaceCompiler := compiler.NewInstructionCompiler(fooChecker.Program, fooChecker.Elaboration) + fooProgram := interfaceCompiler.Compile() + + interfaceVM := vm.NewVM(fooLocation, fooProgram, nil) + fooContractValue, err := interfaceVM.InitializeContract() + require.NoError(t, err) + + // Deploy the imported `Bar` program + + barLocation := common.NewAddressLocation( + nil, + common.Address{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2}, + "Bar", + ) + + barChecker, err := ParseAndCheckWithOptions(t, + ` + contract Bar { + fun sayHello(): String { + return "Hello from Bar!" + } + }`, + ParseAndCheckOptions{ + Config: &sema.Config{ + LocationHandler: singleIdentifierLocationResolver(t), + }, + Location: barLocation, + }, + ) + require.NoError(t, err) + + barCompiler := compiler.NewInstructionCompiler(barChecker.Program, barChecker.Elaboration) + barProgram := barCompiler.Compile() + + barVM := vm.NewVM(barLocation, barProgram, nil) + barContractValue, err := barVM.InitializeContract() + require.NoError(t, err) + + // Define the implementation + + bazLocation := common.NewAddressLocation( + nil, + common.Address{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3}, + "Baz", + ) + + bazChecker, err := ParseAndCheckWithOptions(t, + ` + import Foo from 0x01 + import Bar from 0x02 + + contract Baz { + struct GreetingImpl: Foo.Greetings { + fun sayHello(): String { + return Bar.sayHello() + } + } + }`, + ParseAndCheckOptions{ + Config: &sema.Config{ + ImportHandler: func(_ *sema.Checker, location common.Location, _ ast.Range) (sema.Import, error) { + var elaboration *sema.Elaboration + switch location { + case fooLocation: + elaboration = fooChecker.Elaboration + case barLocation: + elaboration = barChecker.Elaboration + default: + return nil, fmt.Errorf("cannot find import for: %s", location) + } + + return sema.ElaborationImport{ + Elaboration: elaboration, + }, nil + }, + LocationHandler: singleIdentifierLocationResolver(t), + }, + Location: bazLocation, + }, + ) + require.NoError(t, err) + + bazImportHandler := func(location common.Location) *bbq.Program[opcode.Instruction] { + switch location { + case fooLocation: + return fooProgram + case barLocation: + return barProgram + default: + panic(fmt.Errorf("cannot find import for: %s", location)) + } + } + + bazCompiler := compiler.NewInstructionCompiler(bazChecker.Program, bazChecker.Elaboration) + bazCompiler.Config.LocationHandler = singleIdentifierLocationResolver(t) + bazCompiler.Config.ImportHandler = bazImportHandler + bazProgram := bazCompiler.Compile() + + implProgramVMConfig := &vm.Config{ + ImportHandler: bazImportHandler, + ContractValueHandler: func(vmConfig *vm.Config, location common.Location) *vm.CompositeValue { + switch location { + case fooLocation: + return fooContractValue + case barLocation: + return barContractValue + default: + panic(fmt.Errorf("cannot find contract: %s", location)) + } + }, + } + + bazVM := vm.NewVM(bazLocation, bazProgram, implProgramVMConfig) + bazContractValue, err := bazVM.InitializeContract() + require.NoError(t, err) + + // Get `Bar.GreetingsImpl` value + + checker, err := ParseAndCheckWithOptions(t, ` + import Baz from 0x03 + + fun test(): Baz.GreetingImpl { + return Baz.GreetingImpl() + }`, + + ParseAndCheckOptions{ + Config: &sema.Config{ + ImportHandler: func(_ *sema.Checker, location common.Location, _ ast.Range) (sema.Import, error) { + var elaboration *sema.Elaboration + switch location { + case bazLocation: + elaboration = bazChecker.Elaboration + default: + return nil, fmt.Errorf("cannot find import for: %s", location) + } + + return sema.ElaborationImport{ + Elaboration: elaboration, + }, nil + }, + LocationHandler: singleIdentifierLocationResolver(t), + }, + }, + ) + require.NoError(t, err) + + scriptImportHandler := func(location common.Location) *bbq.Program[opcode.Instruction] { + switch location { + case barLocation: + return barProgram + case bazLocation: + return bazProgram + default: + panic(fmt.Errorf("cannot find import for: %s", location)) + } + } + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + comp.Config.LocationHandler = singleIdentifierLocationResolver(t) + comp.Config.ImportHandler = scriptImportHandler + + program := comp.Compile() + + vmConfig := &vm.Config{ + ImportHandler: scriptImportHandler, + ContractValueHandler: func(vmConfig *vm.Config, location common.Location) *vm.CompositeValue { + switch location { + case barLocation: + return barContractValue + case bazLocation: + return bazContractValue + default: + panic(fmt.Errorf("cannot find contract: %s", location)) + } + }, + } + + scriptVM := vm.NewVM(scriptLocation(), program, vmConfig) + implValue, err := scriptVM.Invoke("test") + require.NoError(t, err) + require.Equal(t, 0, scriptVM.StackSize()) + + require.IsType(t, &vm.CompositeValue{}, implValue) + compositeValue := implValue.(*vm.CompositeValue) + require.Equal( + t, + common.TypeID("A.0000000000000003.Baz.GreetingImpl"), + compositeValue.TypeID(), + ) + + // Test Script. This program only imports `Foo` statically. + // But the argument passed into the script is of type `Baz.GreetingImpl`. + // So the linking of `Baz` happens dynamically at runtime. + // However, `Baz` also has an import to `Bar`. So when the + // `Baz` is linked and imported at runtime, its imports also + // should get linked at runtime (similar to how static linking works). + + checker, err = ParseAndCheckWithOptions(t, ` + import Foo from 0x01 + + fun test(v: {Foo.Greetings}): String { + return v.sayHello() + }`, + + ParseAndCheckOptions{ + Config: &sema.Config{ + ImportHandler: func(_ *sema.Checker, location common.Location, _ ast.Range) (sema.Import, error) { + var elaboration *sema.Elaboration + switch location { + case fooLocation: + elaboration = fooChecker.Elaboration + case bazLocation: + elaboration = bazChecker.Elaboration + default: + return nil, fmt.Errorf("cannot find import for: %s", location) + } + + return sema.ElaborationImport{ + Elaboration: elaboration, + }, nil + }, + LocationHandler: singleIdentifierLocationResolver(t), + }, + }, + ) + require.NoError(t, err) + + scriptImportHandler = func(location common.Location) *bbq.Program[opcode.Instruction] { + switch location { + case fooLocation: + return fooProgram + case barLocation: + return barProgram + case bazLocation: + return bazProgram + default: + panic(fmt.Errorf("cannot find import for: %s", location)) + } + + return fooProgram + } + + comp = compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + comp.Config.LocationHandler = singleIdentifierLocationResolver(t) + comp.Config.ImportHandler = scriptImportHandler + + program = comp.Compile() + + vmConfig = &vm.Config{ + ImportHandler: scriptImportHandler, + ContractValueHandler: func(vmConfig *vm.Config, location common.Location) *vm.CompositeValue { + switch location { + case fooLocation: + return fooContractValue + case barLocation: + return barContractValue + case bazLocation: + return bazContractValue + default: + panic(fmt.Errorf("cannot find contract: %s", location)) + } + }, + } + + scriptVM = vm.NewVM(scriptLocation(), program, vmConfig) + + result, err := scriptVM.Invoke("test", implValue) + require.NoError(t, err) + require.Equal(t, 0, scriptVM.StackSize()) + + require.Equal(t, vm.NewStringValue("Hello from Bar!"), result) + }) +} + +func TestArrayLiteral(t *testing.T) { + + t.Parallel() + + t.Run("array literal", func(t *testing.T) { + t.Parallel() + + checker, err := ParseAndCheck(t, ` + fun test(): [Int] { + return [2, 5] + } + `) + require.NoError(t, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + program := comp.Compile() + + vmConfig := &vm.Config{} + vmInstance := vm.NewVM(scriptLocation(), program, vmConfig) + + result, err := vmInstance.Invoke("test") + require.NoError(t, err) + require.Equal(t, 0, vmInstance.StackSize()) + + require.IsType(t, &vm.ArrayValue{}, result) + array := result.(*vm.ArrayValue) + assert.Equal(t, 2, array.Count()) + assert.Equal(t, vm.NewIntValue(2), array.Get(vmConfig, 0)) + assert.Equal(t, vm.NewIntValue(5), array.Get(vmConfig, 1)) + }) + + t.Run("array get", func(t *testing.T) { + t.Parallel() + + checker, err := ParseAndCheck(t, ` + fun test(): Int { + var a = [2, 5, 7, 3] + return a[1] + } + `) + require.NoError(t, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + program := comp.Compile() + + vmConfig := &vm.Config{} + vmInstance := vm.NewVM(scriptLocation(), program, vmConfig) + + result, err := vmInstance.Invoke("test") + require.NoError(t, err) + require.Equal(t, 0, vmInstance.StackSize()) + assert.Equal(t, vm.NewIntValue(5), result) + }) + + t.Run("array set", func(t *testing.T) { + t.Parallel() + + checker, err := ParseAndCheck(t, ` + fun test(): [Int] { + var a = [2, 5, 4] + a[2] = 8 + return a + } + `) + require.NoError(t, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + program := comp.Compile() + + vmConfig := &vm.Config{} + vmInstance := vm.NewVM(scriptLocation(), program, vmConfig) + + result, err := vmInstance.Invoke("test") + require.NoError(t, err) + require.Equal(t, 0, vmInstance.StackSize()) + + require.IsType(t, &vm.ArrayValue{}, result) + array := result.(*vm.ArrayValue) + assert.Equal(t, 3, array.Count()) + assert.Equal(t, vm.NewIntValue(2), array.Get(vmConfig, 0)) + assert.Equal(t, vm.NewIntValue(5), array.Get(vmConfig, 1)) + assert.Equal(t, vm.NewIntValue(8), array.Get(vmConfig, 2)) + }) +} + +func TestReference(t *testing.T) { + + t.Parallel() + + t.Run("method call", func(t *testing.T) { + t.Parallel() + + checker, err := ParseAndCheck(t, ` + struct Foo { + var id : String + + init(_ id: String) { + self.id = id + } + + fun sayHello(_ id: Int): String { + return self.id + } + } + + fun test(): String { + var foo = Foo("Hello from Foo!") + var ref = &foo as &Foo + return ref.sayHello(1) + } + `) + require.NoError(t, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + program := comp.Compile() + + vmConfig := vm.NewConfig(nil) + + vmInstance := vm.NewVM(scriptLocation(), program, vmConfig) + + result, err := vmInstance.Invoke("test") + require.NoError(t, err) + require.Equal(t, 0, vmInstance.StackSize()) + + require.Equal(t, vm.NewStringValue("Hello from Foo!"), result) + }) +} + +func TestResource(t *testing.T) { + + t.Parallel() + + t.Run("new", func(t *testing.T) { + t.Parallel() + + checker, err := ParseAndCheck(t, ` + resource Foo { + var id : Int + + init(_ id: Int) { + self.id = id + } + } + + fun test(): @Foo { + var i = 0 + var r <- create Foo(5) + return <- r + } + `) + require.NoError(t, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + program := comp.Compile() + + vmConfig := &vm.Config{} + vmInstance := vm.NewVM(scriptLocation(), program, vmConfig) + + result, err := vmInstance.Invoke("test") + require.NoError(t, err) + require.Equal(t, 0, vmInstance.StackSize()) + + require.IsType(t, &vm.CompositeValue{}, result) + structValue := result.(*vm.CompositeValue) + compositeType := structValue.CompositeType + + require.Equal(t, "Foo", compositeType.QualifiedIdentifier) + require.Equal( + t, + vm.NewIntValue(5), + structValue.GetMember(vmConfig, "id"), + ) + }) + + t.Run("destroy", func(t *testing.T) { + t.Parallel() + + checker, err := ParseAndCheck(t, ` + resource Foo { + var id : Int + + init(_ id: Int) { + self.id = id + } + } + + fun test() { + var i = 0 + var r <- create Foo(5) + destroy r + } + `) + require.NoError(t, err) + + comp := compiler.NewInstructionCompiler(checker.Program, checker.Elaboration) + program := comp.Compile() + + printProgram("", program) + + vmConfig := &vm.Config{} + vmInstance := vm.NewVM(scriptLocation(), program, vmConfig) + + _, err = vmInstance.Invoke("test") + require.NoError(t, err) + require.Equal(t, 0, vmInstance.StackSize()) + }) +} + +func fib(n int) int { + if n < 2 { + return n + } + return fib(n-1) + fib(n-2) +} + +func BenchmarkGoFib(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + fib(46) + } +} diff --git a/bbq/vm/types.go b/bbq/vm/types.go new file mode 100644 index 0000000000..3af1f23766 --- /dev/null +++ b/bbq/vm/types.go @@ -0,0 +1,29 @@ +/* + * 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/interpreter" + +type StaticType = interpreter.StaticType + +func IsSubType(config *Config, sourceType, targetType StaticType) bool { + // TODO: Avoid conversion to sema types. + inter := config.interpreter() + return inter.IsSubType(sourceType, targetType) +} diff --git a/bbq/vm/value.go b/bbq/vm/value.go new file mode 100644 index 0000000000..ba3cc89a24 --- /dev/null +++ b/bbq/vm/value.go @@ -0,0 +1,57 @@ +/* + * 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/atree" +) + +type Value interface { + isValue() + StaticType(*Config) StaticType + Transfer( + config *Config, + address atree.Address, + remove bool, + storable atree.Storable, + ) Value + String() string +} + +type MemberAccessibleValue interface { + GetMember(config *Config, name string) Value + SetMember(conf *Config, name string, value Value) +} + +type ResourceKindedValue interface { + Value + //Destroy(interpreter *Interpreter, locationRange LocationRange) + //IsDestroyed() bool + //isInvalidatedResource(*Interpreter) bool + IsResourceKinded() bool +} + +// ReferenceTrackedResourceKindedValue is a resource-kinded value +// that must be tracked when a reference of it is taken. +type ReferenceTrackedResourceKindedValue interface { + ResourceKindedValue + IsReferenceTrackedResourceKindedValue() + ValueID() atree.ValueID + IsStaleResource() bool +} diff --git a/bbq/vm/value_account.go b/bbq/vm/value_account.go new file mode 100644 index 0000000000..781892bb9b --- /dev/null +++ b/bbq/vm/value_account.go @@ -0,0 +1,403 @@ +/* + * 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/common" + "github.com/onflow/cadence/errors" + "github.com/onflow/cadence/interpreter" + "github.com/onflow/cadence/sema" + "github.com/onflow/cadence/stdlib" +) + +type AccountIDGenerator interface { + // GenerateAccountID generates a new, *non-zero*, unique ID for the given account. + GenerateAccountID(address common.Address) (uint64, error) +} + +func NewAuthAccountReferenceValue( + conf *Config, + address common.Address, +) *EphemeralReferenceValue { + return newAccountReferenceValue( + conf, + address, + interpreter.FullyEntitledAccountAccess, + ) +} + +func NewAccountReferenceValue( + conf *Config, + address common.Address, +) *EphemeralReferenceValue { + return newAccountReferenceValue( + conf, + address, + interpreter.UnauthorizedAccess, + ) +} + +func newAccountReferenceValue( + conf *Config, + address common.Address, + authorization interpreter.Authorization, +) *EphemeralReferenceValue { + return NewEphemeralReferenceValue( + conf, + newAccountValue(address), + authorization, + interpreter.PrimitiveStaticTypeAccount, + ) +} + +func newAccountValue( + address common.Address, +) *SimpleCompositeValue { + value := &SimpleCompositeValue{ + typeID: sema.AccountType.ID(), + staticType: interpreter.PrimitiveStaticTypeAccount, + Kind: common.CompositeKindStructure, + fields: map[string]Value{ + sema.AccountTypeAddressFieldName: AddressValue(address), + }, + } + + value.computeField = func(name string) Value { + var field Value + switch name { + case sema.AccountTypeStorageFieldName: + field = NewAccountStorageValue(address) + case sema.AccountTypeCapabilitiesFieldName: + field = NewAccountCapabilitiesValue(address) + default: + return nil + } + + value.fields[name] = field + return field + } + + // TODO: add the remaining fields + + return value +} + +// members + +func init() { + // Any member methods goes here +} + +func getAddressMetaInfoFromValue(value Value) common.Address { + simpleCompositeValue, ok := value.(*SimpleCompositeValue) + if !ok { + panic(errors.NewUnreachableError()) + } + + addressMetaInfo := simpleCompositeValue.metadata[sema.AccountTypeAddressFieldName] + address, ok := addressMetaInfo.(common.Address) + if !ok { + panic(errors.NewUnreachableError()) + } + + return address +} + +func getCapability( + config *Config, + address common.Address, + path PathValue, + wantedBorrowType *interpreter.ReferenceStaticType, + borrow bool, +) Value { + var failValue Value + if borrow { + failValue = Nil + } else { + failValue = + NewInvalidCapabilityValue( + address, + wantedBorrowType, + ) + } + + domain := path.Domain.Identifier() + identifier := path.Identifier + + // Read stored capability, if any + + readValue := ReadStored( + config.MemoryGauge, + config.Storage, + address, + domain, + identifier, + ) + if readValue == nil { + return failValue + } + + var readCapabilityValue CapabilityValue + switch readValue := readValue.(type) { + case CapabilityValue: + readCapabilityValue = readValue + default: + panic(errors.NewUnreachableError()) + } + + capabilityBorrowType, ok := readCapabilityValue.BorrowType.(*interpreter.ReferenceStaticType) + if !ok { + panic(errors.NewUnreachableError()) + } + + capabilityID := readCapabilityValue.ID + capabilityAddress := readCapabilityValue.Address + + var resultValue Value + if borrow { + // When borrowing, + // check the controller and types, + // and return a checked reference + + resultValue = BorrowCapabilityController( + config, + capabilityAddress, + capabilityID, + wantedBorrowType, + capabilityBorrowType, + ) + } else { + // When not borrowing, + // check the controller and types, + // and return a capability + + controller, resultBorrowType := getCheckedCapabilityController( + config, + capabilityAddress, + capabilityID, + wantedBorrowType, + capabilityBorrowType, + ) + if controller != nil { + resultValue = NewCapabilityValue( + capabilityAddress, + capabilityID, + resultBorrowType, + ) + } + } + + if resultValue == nil { + return failValue + } + + if borrow { + resultValue = NewSomeValueNonCopying( + resultValue, + ) + } + + return resultValue +} + +func BorrowCapabilityController( + config *Config, + capabilityAddress AddressValue, + capabilityID IntValue, + wantedBorrowType *interpreter.ReferenceStaticType, + capabilityBorrowType *interpreter.ReferenceStaticType, +) ReferenceValue { + referenceValue := GetCheckedCapabilityControllerReference( + config, + capabilityAddress, + capabilityID, + wantedBorrowType, + capabilityBorrowType, + ) + if referenceValue == nil { + return nil + } + + // Attempt to dereference, + // which reads the stored value + // and performs a dynamic type check + + referencedValue := referenceValue.ReferencedValue( + config, + false, + ) + if referencedValue == nil { + return nil + } + + return referenceValue +} + +func checkAndIssueStorageCapabilityControllerWithType( + config *Config, + idGenerator AccountIDGenerator, + address common.Address, + targetPathValue PathValue, + ty StaticType, +) CapabilityValue { + + borrowType, ok := ty.(*interpreter.ReferenceStaticType) + if !ok { + // TODO: remove conversion. se static type in error + semaType, err := config.interpreter().ConvertStaticToSemaType(ty) + if err != nil { + panic(err) + } + panic(interpreter.InvalidCapabilityIssueTypeError{ + ExpectedTypeDescription: "reference type", + ActualType: semaType, + }) + } + + // Issue capability controller + + capabilityIDValue := IssueStorageCapabilityController( + config, + idGenerator, + address, + borrowType, + targetPathValue, + ) + + if capabilityIDValue == InvalidCapabilityID { + panic(interpreter.InvalidCapabilityIDError{}) + } + + // Return controller's capability + + return NewCapabilityValue( + AddressValue(address), + capabilityIDValue, + borrowType, + ) +} + +func IssueStorageCapabilityController( + config *Config, + idGenerator AccountIDGenerator, + address common.Address, + borrowType *interpreter.ReferenceStaticType, + targetPathValue PathValue, +) IntValue { + // Create and write StorageCapabilityController + + var capabilityID uint64 + var err error + errors.WrapPanic(func() { + capabilityID, err = idGenerator.GenerateAccountID(address) + }) + if err != nil { + panic(interpreter.WrappedExternalError(err)) + } + if capabilityID == 0 { + panic(errors.NewUnexpectedError("invalid zero account ID")) + } + + capabilityIDValue := NewIntValue(int64(capabilityID)) + + controller := NewStorageCapabilityControllerValue( + borrowType, + capabilityIDValue, + targetPathValue, + ) + + storeCapabilityController(config, address, capabilityIDValue, controller) + recordStorageCapabilityController(config, address, targetPathValue, capabilityIDValue) + + return capabilityIDValue +} + +func storeCapabilityController( + config *Config, + address common.Address, + capabilityIDValue IntValue, + controller CapabilityControllerValue, +) { + storageMapKey := interpreter.Uint64StorageMapKey(capabilityIDValue.SmallInt) + + existed := WriteStored( + config, + address, + stdlib.CapabilityControllerStorageDomain, + storageMapKey, + controller, + ) + + if existed { + panic(errors.NewUnreachableError()) + } +} + +var capabilityIDSetStaticType = &interpreter.DictionaryStaticType{ + KeyType: interpreter.PrimitiveStaticTypeUInt64, + ValueType: interpreter.NilStaticType, +} + +func recordStorageCapabilityController( + config *Config, + address common.Address, + targetPathValue PathValue, + capabilityIDValue IntValue, +) { + if targetPathValue.Domain != common.PathDomainStorage { + panic(errors.NewUnreachableError()) + } + + addressPath := AddressPath{ + Address: address, + Path: targetPathValue, + } + if config.CapabilityControllerIterations[addressPath] > 0 { + config.MutationDuringCapabilityControllerIteration = true + } + + identifier := targetPathValue.Identifier + + storageMapKey := interpreter.StringStorageMapKey(identifier) + + accountStorage := config.Storage.GetStorageMap(address, stdlib.PathCapabilityStorageDomain, true) + + referenced := accountStorage.ReadValue(config.MemoryGauge, interpreter.StringStorageMapKey(identifier)) + readValue := InterpreterValueToVMValue(config.Storage, referenced) + + setKey := capabilityIDValue + setValue := Nil + + if readValue == nil { + capabilityIDSet := NewDictionaryValue( + config, + capabilityIDSetStaticType, + setKey, + setValue, + ) + capabilityIDSetInterValue := VMValueToInterpreterValue(config, capabilityIDSet) + accountStorage.SetValue(config.interpreter(), storageMapKey, capabilityIDSetInterValue) + } else { + capabilityIDSet := readValue.(*DictionaryValue) + existing := capabilityIDSet.Insert(config, setKey, setValue) + if existing != Nil { + panic(errors.NewUnreachableError()) + } + } +} diff --git a/bbq/vm/value_account_capabilities.go b/bbq/vm/value_account_capabilities.go new file mode 100644 index 0000000000..7cac4796ea --- /dev/null +++ b/bbq/vm/value_account_capabilities.go @@ -0,0 +1,185 @@ +/* + * 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/atree" + + "github.com/onflow/cadence/common" + "github.com/onflow/cadence/errors" + "github.com/onflow/cadence/interpreter" + "github.com/onflow/cadence/sema" +) + +func NewAccountCapabilitiesValue(accountAddress common.Address) *SimpleCompositeValue { + value := &SimpleCompositeValue{ + typeID: sema.Account_StorageType.ID(), + staticType: interpreter.PrimitiveStaticTypeAccount_Capabilities, + Kind: common.CompositeKindStructure, + fields: map[string]Value{ + // TODO: add the remaining fields + }, + metadata: map[string]any{ + sema.AccountTypeAddressFieldName: accountAddress, + }, + } + + value.computeField = func(name string) Value { + var field Value + switch name { + case sema.Account_CapabilitiesTypeStorageFieldName: + field = NewAccountStorageCapabilitiesValue(accountAddress) + default: + return nil + } + + value.fields[name] = field + return field + } + + return value +} + +// members + +func init() { + accountCapabilitiesTypeName := sema.Account_CapabilitiesType.QualifiedIdentifier() + + // Account.Capabilities.get + RegisterTypeBoundFunction( + accountCapabilitiesTypeName, + sema.Account_CapabilitiesTypeGetFunctionName, + NativeFunctionValue{ + ParameterCount: len(sema.Account_CapabilitiesTypeGetFunctionType.Parameters), + Function: func(config *Config, typeArguments []StaticType, args ...Value) Value { + // Get address field from the receiver (Account.Capabilities) + address := getAddressMetaInfoFromValue(args[0]) + + // Path argument + path, ok := args[1].(PathValue) + if !ok { + panic(errors.NewUnreachableError()) + } + + //pathStaticType := path.StaticType(config.MemoryGauge) + // + //if !IsSubType(pathStaticType, pathType) { + // panic(fmt.Errorf("type mismatch")) + //} + + // NOTE: the type parameter is optional, for backwards compatibility + + var borrowType *interpreter.ReferenceStaticType + if len(typeArguments) > 0 { + ty := typeArguments[0] + // we handle the nil case for this below + borrowType, _ = ty.(*interpreter.ReferenceStaticType) + } + + return getCapability( + config, + address, + path, + borrowType, + false, + ) + }, + }) + + // Account.Capabilities.publish + RegisterTypeBoundFunction( + accountCapabilitiesTypeName, + sema.Account_CapabilitiesTypePublishFunctionName, + NativeFunctionValue{ + ParameterCount: len(sema.Account_CapabilitiesTypePublishFunctionType.Parameters), + Function: func(config *Config, typeArguments []StaticType, args ...Value) Value { + // Get address field from the receiver (Account.Capabilities) + accountAddress := getAddressMetaInfoFromValue(args[0]) + + // Get capability argument + + var capabilityValue CapabilityValue + switch firstValue := args[1].(type) { + case CapabilityValue: + capabilityValue = firstValue + default: + panic(errors.NewUnreachableError()) + } + + capabilityAddressValue := common.Address(capabilityValue.Address) + if capabilityAddressValue != accountAddress { + panic(interpreter.CapabilityAddressPublishingError{ + CapabilityAddress: interpreter.AddressValue(capabilityAddressValue), + AccountAddress: interpreter.AddressValue(accountAddress), + }) + } + + // Get path argument + + path, ok := args[2].(PathValue) + if !ok { + panic(errors.NewUnreachableError()) + } + + if !ok || path.Domain != common.PathDomainPublic { + panic(errors.NewUnreachableError()) + } + + domain := path.Domain.Identifier() + identifier := path.Identifier + + // Prevent an overwrite + + storageMapKey := interpreter.StringStorageMapKey(identifier) + if StoredValueExists( + config.Storage, + accountAddress, + domain, + storageMapKey, + ) { + panic(interpreter.OverwriteError{ + Address: interpreter.AddressValue(accountAddress), + Path: VMValueToInterpreterValue(config, path).(interpreter.PathValue), + }) + } + + capabilityValue, ok = capabilityValue.Transfer( + config, + atree.Address(accountAddress), + true, + nil, + ).(CapabilityValue) + if !ok { + panic(errors.NewUnreachableError()) + } + + // Write new value + + WriteStored( + config, + accountAddress, + domain, + storageMapKey, + capabilityValue, + ) + + return Void + }, + }) +} diff --git a/bbq/vm/value_account_storage.go b/bbq/vm/value_account_storage.go new file mode 100644 index 0000000000..18f29180d2 --- /dev/null +++ b/bbq/vm/value_account_storage.go @@ -0,0 +1,141 @@ +/* + * 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/atree" + + "github.com/onflow/cadence/common" + "github.com/onflow/cadence/errors" + "github.com/onflow/cadence/interpreter" + "github.com/onflow/cadence/sema" +) + +func NewAccountStorageValue(accountAddress common.Address) *SimpleCompositeValue { + return &SimpleCompositeValue{ + typeID: sema.Account_StorageType.ID(), + staticType: interpreter.PrimitiveStaticTypeAccount_Storage, + Kind: common.CompositeKindStructure, + fields: map[string]Value{ + // TODO: add the remaining fields + }, + metadata: map[string]any{ + sema.AccountTypeAddressFieldName: accountAddress, + }, + } +} + +// members + +func init() { + + accountStorageTypeName := sema.Account_StorageType.QualifiedIdentifier() + + // Account.Storage.save + RegisterTypeBoundFunction( + accountStorageTypeName, + sema.Account_StorageTypeSaveFunctionName, + NativeFunctionValue{ + ParameterCount: len(sema.Account_StorageTypeSaveFunctionType.Parameters), + Function: func(config *Config, typeArs []StaticType, args ...Value) Value { + address := getAddressMetaInfoFromValue(args[0]) + + value := args[1] + + path, ok := args[2].(PathValue) + if !ok { + panic(errors.NewUnreachableError()) + } + + domain := path.Domain.Identifier() + identifier := path.Identifier + + // Prevent an overwrite + + //if interpreter.storedValueExists( + // address, + // domain, + // identifier, + //) { + // panic("overwrite error") + //} + + value = value.Transfer( + config, + atree.Address(address), + true, + nil, + ) + + // Write new value + + WriteStored( + config, + address, + domain, + interpreter.StringStorageMapKey(identifier), + value, + ) + + return VoidValue{} + }, + }) + + // Account.Storage.borrow + RegisterTypeBoundFunction( + accountStorageTypeName, + sema.Account_StorageTypeBorrowFunctionName, + NativeFunctionValue{ + ParameterCount: len(sema.Account_StorageTypeBorrowFunctionType.Parameters), + Function: func(config *Config, typeArgs []StaticType, args ...Value) Value { + address := getAddressMetaInfoFromValue(args[0]) + + path, ok := args[1].(PathValue) + if !ok { + panic(errors.NewUnreachableError()) + } + + referenceType, ok := typeArgs[0].(*interpreter.ReferenceStaticType) + if !ok { + panic(errors.NewUnreachableError()) + } + + reference := NewStorageReferenceValue( + referenceType.Authorization, + address, + path, + referenceType.ReferencedType, + ) + + // Attempt to dereference, + // which reads the stored value + // and performs a dynamic type check + + referenced, err := reference.dereference(config) + if err != nil { + panic(err) + } + if referenced == nil { + return NilValue{} + } + + return NewSomeValueNonCopying(reference) + }, + }) +} diff --git a/bbq/vm/value_account_storagecapabilities.go b/bbq/vm/value_account_storagecapabilities.go new file mode 100644 index 0000000000..05d02b213b --- /dev/null +++ b/bbq/vm/value_account_storagecapabilities.go @@ -0,0 +1,81 @@ +/* + * 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/common" + "github.com/onflow/cadence/errors" + "github.com/onflow/cadence/interpreter" + "github.com/onflow/cadence/sema" +) + +func NewAccountStorageCapabilitiesValue(accountAddress common.Address) *SimpleCompositeValue { + return &SimpleCompositeValue{ + typeID: sema.Account_StorageCapabilitiesType.ID(), + staticType: interpreter.PrimitiveStaticTypeAccount_StorageCapabilities, + Kind: common.CompositeKindStructure, + fields: map[string]Value{ + // TODO: add the remaining fields + }, + metadata: map[string]any{ + sema.AccountTypeAddressFieldName: accountAddress, + }, + } +} + +// members + +func init() { + accountStorageCapabilitiesTypeName := sema.Account_StorageCapabilitiesType.QualifiedIdentifier() + + // Account.StorageCapabilities.issue + RegisterTypeBoundFunction( + accountStorageCapabilitiesTypeName, + sema.Account_StorageCapabilitiesTypeIssueFunctionName, + NativeFunctionValue{ + ParameterCount: len(sema.Account_StorageCapabilitiesTypeIssueFunctionType.Parameters), + Function: func(config *Config, typeArguments []StaticType, args ...Value) Value { + // Get address field from the receiver (Account.StorageCapabilities) + accountAddress := getAddressMetaInfoFromValue(args[0]) + + // Path argument + targetPathValue, ok := args[1].(PathValue) + if !ok { + panic(errors.NewUnreachableError()) + } + + if !ok || targetPathValue.Domain != common.PathDomainStorage { + panic(errors.NewUnreachableError()) + } + + // Get borrow type type-argument + ty := typeArguments[0] + + // Issue capability controller and return capability + + return checkAndIssueStorageCapabilityControllerWithType( + config, + config.AccountHandler, + accountAddress, + targetPathValue, + ty, + ) + }, + }) +} diff --git a/bbq/vm/value_address.go b/bbq/vm/value_address.go new file mode 100644 index 0000000000..7510eddb9d --- /dev/null +++ b/bbq/vm/value_address.go @@ -0,0 +1,45 @@ +/* + * 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/atree" + + "github.com/onflow/cadence/common" + "github.com/onflow/cadence/format" + "github.com/onflow/cadence/interpreter" +) + +type AddressValue common.Address + +var _ Value = AddressValue{} + +func (AddressValue) isValue() {} + +func (AddressValue) StaticType(*Config) StaticType { + return interpreter.PrimitiveStaticTypeAddress +} + +func (v AddressValue) Transfer(*Config, atree.Address, bool, atree.Storable) Value { + return v +} + +func (v AddressValue) String() string { + return format.Address(common.Address(v)) +} diff --git a/bbq/vm/value_array.go b/bbq/vm/value_array.go new file mode 100644 index 0000000000..cd71d04ab5 --- /dev/null +++ b/bbq/vm/value_array.go @@ -0,0 +1,420 @@ +/* + * 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 ( + goerrors "errors" + "github.com/onflow/atree" + "github.com/onflow/cadence/common" + "github.com/onflow/cadence/errors" + "github.com/onflow/cadence/interpreter" +) + +type ArrayValue struct { + Type interpreter.ArrayStaticType + array *atree.Array + isResourceKinded bool + elementSize uint + isDestroyed bool +} + +var _ Value = &ArrayValue{} +var _ ReferenceTrackedResourceKindedValue = &ArrayValue{} + +func NewArrayValue( + config *Config, + arrayType interpreter.ArrayStaticType, + isResourceKinded bool, + values ...Value, +) *ArrayValue { + + address := common.ZeroAddress + + var index int + count := len(values) + + return NewArrayValueWithIterator( + config, + arrayType, + isResourceKinded, + address, + func() Value { + if index >= count { + return nil + } + + value := values[index] + + index++ + + value = value.Transfer( + config, + atree.Address(address), + true, + nil, + ) + + return value + }, + ) +} + +func NewArrayValueWithIterator( + config *Config, + arrayType interpreter.ArrayStaticType, + isResourceKinded bool, + address common.Address, + values func() Value, +) *ArrayValue { + constructor := func() *atree.Array { + array, err := atree.NewArrayFromBatchData( + config.Storage, + atree.Address(address), + arrayType, + func() (atree.Value, error) { + vmValue := values() + value := VMValueToInterpreterValue(config, vmValue) + return value, nil + }, + ) + if err != nil { + panic(errors.NewExternalError(err)) + } + return array + } + + return newArrayValueFromConstructor(arrayType, isResourceKinded, constructor) +} + +func newArrayValueFromConstructor( + staticType interpreter.ArrayStaticType, + isResourceKinded bool, + constructor func() *atree.Array, +) *ArrayValue { + + elementSize := interpreter.ArrayElementSize(staticType) + + return newArrayValueFromAtreeArray( + staticType, + isResourceKinded, + elementSize, + constructor(), + ) +} + +func newArrayValueFromAtreeArray( + staticType interpreter.ArrayStaticType, + isResourceKinded bool, + elementSize uint, + atreeArray *atree.Array, +) *ArrayValue { + return &ArrayValue{ + Type: staticType, + array: atreeArray, + elementSize: elementSize, + isResourceKinded: isResourceKinded, + } +} + +func (v *ArrayValue) isValue() { + panic("implement me") +} + +func (v *ArrayValue) StaticType(*Config) StaticType { + return v.Type +} + +func (v *ArrayValue) Transfer(config *Config, address atree.Address, remove bool, storable atree.Storable) Value { + + storage := config.Storage + + array := v.array + + //currentValueID := v.ValueID() + + //if preventTransfer == nil { + // preventTransfer = map[atree.ValueID]struct{}{} + //} else if _, ok := preventTransfer[currentValueID]; ok { + // panic(RecursiveTransferError{ + // LocationRange: locationRange, + // }) + //} + //preventTransfer[currentValueID] = struct{}{} + //defer delete(preventTransfer, currentValueID) + + needsStoreTo := v.NeedsStoreTo(address) + isResourceKinded := v.IsResourceKinded() + + if needsStoreTo || !isResourceKinded { + + // Use non-readonly iterator here because iterated + // value can be removed if remove parameter is true. + iterator, err := v.array.Iterator() + if err != nil { + panic(errors.NewExternalError(err)) + } + + array, err = atree.NewArrayFromBatchData( + config.Storage, + address, + v.array.Type(), + func() (atree.Value, error) { + value, err := iterator.Next() + if err != nil { + return nil, err + } + if value == nil { + return nil, nil + } + + element := interpreter.MustConvertStoredValue(config.MemoryGauge, value) + + // TODO: Transfer before returning. + return element, nil + }, + ) + if err != nil { + panic(errors.NewExternalError(err)) + } + + if remove { + err = v.array.PopIterate(func(valueStorable atree.Storable) { + RemoveReferencedSlab(storage, valueStorable) + }) + if err != nil { + panic(errors.NewExternalError(err)) + } + + //interpreter.maybeValidateAtreeValue(v.array) + //if hasNoParentContainer { + // interpreter.maybeValidateAtreeStorage() + //} + + RemoveReferencedSlab(storage, storable) + } + } + + if isResourceKinded { + // Update the resource in-place, + // and also update all values that are referencing the same value + // (but currently point to an outdated Go instance of the value) + + // If checking of transfers of invalidated resource is enabled, + // then mark the resource array as invalidated, by unsetting the backing array. + // This allows raising an error when the resource array is attempted + // to be transferred/moved again (see beginning of this function) + + invalidateReferencedResources(config, v) + + v.array = nil + } + + res := newArrayValueFromAtreeArray( + v.Type, + isResourceKinded, + v.elementSize, + array, + ) + + res.isResourceKinded = v.isResourceKinded + res.isDestroyed = v.isDestroyed + + return res +} + +func (v *ArrayValue) String() string { + panic("implement me") +} + +func (v *ArrayValue) ValueID() atree.ValueID { + return v.array.ValueID() +} + +func (v *ArrayValue) SlabID() atree.SlabID { + return v.array.SlabID() +} + +func (v *ArrayValue) StorageAddress() atree.Address { + return v.array.Address() +} + +func (v *ArrayValue) NeedsStoreTo(address atree.Address) bool { + return address != v.StorageAddress() +} + +func (v *ArrayValue) IsResourceKinded() bool { + return v.isResourceKinded +} + +func (v *ArrayValue) IsReferenceTrackedResourceKindedValue() {} + +func (v *ArrayValue) IsStaleResource() bool { + return v.array == nil && v.IsResourceKinded() +} + +func (v *ArrayValue) Count() int { + return int(v.array.Count()) +} + +func (v *ArrayValue) Get(config *Config, index int) Value { + + // We only need to check the lower bound before converting from `int` (signed) to `uint64` (unsigned). + // atree's Array.Get function will check the upper bound and report an atree.IndexOutOfBoundsError + + if index < 0 { + panic(interpreter.ArrayIndexOutOfBoundsError{ + Index: index, + Size: v.Count(), + }) + } + + storedValue, err := v.array.Get(uint64(index)) + if err != nil { + v.handleIndexOutOfBoundsError(err, index) + panic(errors.NewExternalError(err)) + } + + return MustConvertStoredValue( + config.MemoryGauge, + config.Storage, + storedValue, + ) +} + +func (v *ArrayValue) handleIndexOutOfBoundsError(err error, index int) { + var indexOutOfBoundsError *atree.IndexOutOfBoundsError + if goerrors.As(err, &indexOutOfBoundsError) { + panic(interpreter.ArrayIndexOutOfBoundsError{ + Index: index, + Size: v.Count(), + }) + } +} + +func (v *ArrayValue) Set(config *Config, index int, element Value) { + + // TODO: + //interpreter.validateMutation(v.ValueID(), locationRange) + + // We only need to check the lower bound before converting from `int` (signed) to `uint64` (unsigned). + // atree's Array.Set function will check the upper bound and report an atree.IndexOutOfBoundsError + + if index < 0 { + panic(interpreter.ArrayIndexOutOfBoundsError{ + Index: index, + Size: v.Count(), + }) + } + + // TODO: + //interpreter.checkContainerMutation(v.Type.ElementType(), element, locationRange) + + element = element.Transfer( + config, + v.array.Address(), + true, + nil, + ) + + storableElement := VMValueToInterpreterValue(config, element) + + existingStorable, err := v.array.Set(uint64(index), storableElement) + if err != nil { + v.handleIndexOutOfBoundsError(err, index) + + panic(errors.NewExternalError(err)) + } + + //interpreter.maybeValidateAtreeValue(v.array) + //interpreter.maybeValidateAtreeStorage() + + existingValue := StoredValue(config.MemoryGauge, existingStorable, config.Storage) + _ = existingValue + + //interpreter.checkResourceLoss(existingValue, locationRange) + + //existingValue.DeepRemove(interpreter, true) // existingValue is standalone because it was overwritten in parent container. + + RemoveReferencedSlab(config.Storage, existingStorable) +} + +func (v *ArrayValue) Iterate( + config *Config, + f func(element Value) (resume bool), + transferElements bool, +) { + v.iterate( + config, + v.array.Iterate, + f, + transferElements, + ) +} + +// IterateReadOnlyLoaded iterates over all LOADED elements of the array. +// DO NOT perform storage mutations in the callback! +func (v *ArrayValue) IterateReadOnlyLoaded( + config *Config, + f func(element Value) (resume bool), +) { + const transferElements = false + + v.iterate( + config, + v.array.IterateReadOnlyLoadedValues, + f, + transferElements, + ) +} + +func (v *ArrayValue) iterate( + config *Config, + atreeIterate func(fn atree.ArrayIterationFunc) error, + f func(element Value) (resume bool), + transferElements bool, +) { + iterate := func() { + err := atreeIterate(func(element atree.Value) (resume bool, err error) { + // atree.Array iteration provides low-level atree.Value, + // convert to high-level interpreter.Value + elementValue := MustConvertStoredValue(config, config, element) + checkInvalidatedResourceOrResourceReference(elementValue) + + if transferElements { + // Each element must be transferred before passing onto the function. + elementValue = elementValue.Transfer( + config, + atree.Address{}, + false, + nil, + ) + } + + resume = f(elementValue) + + return resume, nil + }) + if err != nil { + panic(errors.NewExternalError(err)) + } + } + + iterate() + //interpreter.withMutationPrevention(v.ValueID(), iterate) +} diff --git a/bbq/vm/value_bool.go b/bbq/vm/value_bool.go new file mode 100644 index 0000000000..1c4997d2ab --- /dev/null +++ b/bbq/vm/value_bool.go @@ -0,0 +1,47 @@ +/* + * 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/atree" + + "github.com/onflow/cadence/format" + "github.com/onflow/cadence/interpreter" +) + +var TrueValue Value = BoolValue(true) +var FalseValue Value = BoolValue(false) + +type BoolValue bool + +var _ Value = BoolValue(true) + +func (BoolValue) isValue() {} + +func (BoolValue) StaticType(*Config) StaticType { + return interpreter.PrimitiveStaticTypeBool +} + +func (v BoolValue) Transfer(*Config, atree.Address, bool, atree.Storable) Value { + return v +} + +func (v BoolValue) String() string { + return format.Bool(bool(v)) +} diff --git a/bbq/vm/value_capability.go b/bbq/vm/value_capability.go new file mode 100644 index 0000000000..a7db209383 --- /dev/null +++ b/bbq/vm/value_capability.go @@ -0,0 +1,253 @@ +/* + * 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/atree" + "github.com/onflow/cadence/common" + "github.com/onflow/cadence/errors" + "github.com/onflow/cadence/format" + "github.com/onflow/cadence/interpreter" + "github.com/onflow/cadence/sema" + "github.com/onflow/cadence/stdlib" +) + +// members + +type CapabilityValue struct { + Address AddressValue + BorrowType StaticType + ID IntValue // TODO: UInt64Value +} + +var _ Value = CapabilityValue{} + +func NewCapabilityValue(address AddressValue, id IntValue, borrowType StaticType) CapabilityValue { + return CapabilityValue{ + Address: address, + BorrowType: borrowType, + ID: id, + } +} + +func NewInvalidCapabilityValue( + address common.Address, + borrowType StaticType, +) CapabilityValue { + return CapabilityValue{ + ID: InvalidCapabilityID, + Address: AddressValue(address), + BorrowType: borrowType, + } +} + +func (CapabilityValue) isValue() {} + +func (v CapabilityValue) StaticType(config *Config) StaticType { + return interpreter.NewCapabilityStaticType(config.MemoryGauge, v.BorrowType) +} + +func (v CapabilityValue) Transfer(*Config, atree.Address, bool, atree.Storable) Value { + return v +} + +func (v CapabilityValue) String() string { + var borrowType string + if v.BorrowType != nil { + borrowType = v.BorrowType.String() + } + return format.Capability( + borrowType, + v.Address.String(), + v.ID.String(), + ) +} + +var InvalidCapabilityID = IntValue{} + +func init() { + typeName := interpreter.PrimitiveStaticTypeCapability.String() + + // Capability.borrow + RegisterTypeBoundFunction( + typeName, + sema.CapabilityTypeBorrowFunctionName, + NativeFunctionValue{ + ParameterCount: 0, + Function: func(config *Config, typeArguments []StaticType, args ...Value) Value { + capabilityValue := getReceiver[CapabilityValue](config, args[0]) + capabilityID := capabilityValue.ID + + if capabilityID == InvalidCapabilityID { + return Nil + } + + capabilityBorrowType := capabilityValue.BorrowType.(*interpreter.ReferenceStaticType) + + var wantedBorrowType *interpreter.ReferenceStaticType + if len(typeArguments) > 0 { + wantedBorrowType = typeArguments[0].(*interpreter.ReferenceStaticType) + } + + address := capabilityValue.Address + + referenceValue := GetCheckedCapabilityControllerReference( + config, + address, + capabilityID, + wantedBorrowType, + capabilityBorrowType, + ) + if referenceValue == nil { + return nil + } + + // TODO: Is this needed? + // Attempt to dereference, + // which reads the stored value + // and performs a dynamic type check + + //value, err := referenceValue.dereference(config.MemoryGauge) + //if err != nil { + // panic(err) + //} + + if referenceValue == nil { + return Nil + } + + return NewSomeValueNonCopying(referenceValue) + }, + }) +} + +func GetCheckedCapabilityControllerReference( + config *Config, + capabilityAddressValue AddressValue, + capabilityIDValue IntValue, + wantedBorrowType *interpreter.ReferenceStaticType, + capabilityBorrowType *interpreter.ReferenceStaticType, +) ReferenceValue { + controller, resultBorrowType := getCheckedCapabilityController( + config, + capabilityAddressValue, + capabilityIDValue, + wantedBorrowType, + capabilityBorrowType, + ) + if controller == nil { + return nil + } + + capabilityAddress := common.Address(capabilityAddressValue) + + return controller.ReferenceValue( + capabilityAddress, + resultBorrowType, + ) +} + +func getCheckedCapabilityController( + config *Config, + capabilityAddressValue AddressValue, + capabilityIDValue IntValue, + wantedBorrowType *interpreter.ReferenceStaticType, + capabilityBorrowType *interpreter.ReferenceStaticType, +) ( + CapabilityControllerValue, + *interpreter.ReferenceStaticType, +) { + if wantedBorrowType == nil { + wantedBorrowType = capabilityBorrowType + } else { + //wantedBorrowType = inter.SubstituteMappedEntitlements(wantedBorrowType).(*sema.ReferenceType) + + if !canBorrow(config, wantedBorrowType, capabilityBorrowType) { + return nil, nil + } + } + + capabilityAddress := common.Address(capabilityAddressValue) + capabilityID := uint64(capabilityIDValue.SmallInt) + + controller := getCapabilityController(config, capabilityAddress, capabilityID) + if controller == nil { + return nil, nil + } + + controllerBorrowType := controller.CapabilityControllerBorrowType() + if !canBorrow(config, wantedBorrowType, controllerBorrowType) { + return nil, nil + } + + return controller, wantedBorrowType +} + +// getCapabilityController gets the capability controller for the given capability ID +func getCapabilityController( + config *Config, + address common.Address, + capabilityID uint64, +) CapabilityControllerValue { + + storageMapKey := interpreter.Uint64StorageMapKey(capabilityID) + + accountStorage := config.Storage.GetStorageMap(address, stdlib.CapabilityControllerStorageDomain, false) + if accountStorage == nil { + return nil + } + + referenced := accountStorage.ReadValue(config.MemoryGauge, storageMapKey) + vmReferencedValue := InterpreterValueToVMValue(config.Storage, referenced) + + controller, ok := vmReferencedValue.(CapabilityControllerValue) + if !ok { + panic(errors.NewUnreachableError()) + } + + return controller +} + +func canBorrow( + config *Config, + wantedBorrowType *interpreter.ReferenceStaticType, + capabilityBorrowType *interpreter.ReferenceStaticType, +) bool { + + // Ensure the wanted borrow type is not more permissive than the capability borrow type + // TODO: + //if !wantedBorrowType.Authorization. + // PermitsAccess(capabilityBorrowType.Authorization) { + // + // return false + //} + + // Ensure the wanted borrow type is a subtype or supertype of the capability borrow type + + return IsSubType( + config, + wantedBorrowType.ReferencedType, + capabilityBorrowType.ReferencedType, + ) || + IsSubType( + config, + capabilityBorrowType.ReferencedType, + wantedBorrowType.ReferencedType, + ) +} diff --git a/bbq/vm/value_capability_controller.go b/bbq/vm/value_capability_controller.go new file mode 100644 index 0000000000..deda75f747 --- /dev/null +++ b/bbq/vm/value_capability_controller.go @@ -0,0 +1,219 @@ +/* + * 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/atree" + "github.com/onflow/cadence/common" + "github.com/onflow/cadence/errors" + "github.com/onflow/cadence/format" + "github.com/onflow/cadence/interpreter" + "github.com/onflow/cadence/sema" +) + +type CapabilityControllerValue interface { + Value + isCapabilityControllerValue() + ReferenceValue( + capabilityAddress common.Address, + resultBorrowType *interpreter.ReferenceStaticType, + ) ReferenceValue + ControllerCapabilityID() IntValue // TODO: UInt64Value + CapabilityControllerBorrowType() *interpreter.ReferenceStaticType +} + +// StorageCapabilityControllerValue + +type StorageCapabilityControllerValue struct { + BorrowType *interpreter.ReferenceStaticType + CapabilityID IntValue + TargetPath PathValue + + // deleted indicates if the controller got deleted. Not stored + deleted bool + + // Injected functions. + // Tags are not stored directly inside the controller + // to avoid unnecessary storage reads + // when the controller is loaded for borrowing/checking + GetCapability func(config *Config) *CapabilityValue + GetTag func(config *Config) *StringValue + SetTag func(config *Config, tag *StringValue) + Delete func(config *Config, locationRange interpreter.LocationRange) + SetTarget func(config *Config, locationRange interpreter.LocationRange, target PathValue) +} + +func NewUnmeteredStorageCapabilityControllerValue( + borrowType *interpreter.ReferenceStaticType, + capabilityID IntValue, + targetPath PathValue, +) *StorageCapabilityControllerValue { + return &StorageCapabilityControllerValue{ + BorrowType: borrowType, + TargetPath: targetPath, + CapabilityID: capabilityID, + } +} + +func NewStorageCapabilityControllerValue( + borrowType *interpreter.ReferenceStaticType, + capabilityID IntValue, + targetPath PathValue, +) *StorageCapabilityControllerValue { + return NewUnmeteredStorageCapabilityControllerValue( + borrowType, + capabilityID, + targetPath, + ) +} + +var _ Value = &StorageCapabilityControllerValue{} +var _ CapabilityControllerValue = &StorageCapabilityControllerValue{} +var _ MemberAccessibleValue = &StorageCapabilityControllerValue{} + +func (*StorageCapabilityControllerValue) isValue() {} + +func (*StorageCapabilityControllerValue) isCapabilityControllerValue() {} + +func (v *StorageCapabilityControllerValue) CapabilityControllerBorrowType() *interpreter.ReferenceStaticType { + return v.BorrowType +} + +func (v *StorageCapabilityControllerValue) StaticType(*Config) StaticType { + return interpreter.PrimitiveStaticTypeStorageCapabilityController +} + +func (v *StorageCapabilityControllerValue) String() string { + // TODO: call recursive string + return format.StorageCapabilityController( + v.BorrowType.String(), + v.CapabilityID.String(), + v.TargetPath.String(), + ) +} + +func (v *StorageCapabilityControllerValue) Transfer( + config *Config, + address atree.Address, + remove bool, + storable atree.Storable, +) Value { + //if remove { + // interpreter.RemoveReferencedSlab(storable) + //} + return v +} + +func (v *StorageCapabilityControllerValue) GetMember(config *Config, name string) (result Value) { + //defer func() { + // switch typedResult := result.(type) { + // case deletionCheckedFunctionValue: + // result = typedResult.FunctionValue + // case FunctionValue: + // panic(errors.NewUnexpectedError("functions need to check deletion. Use newHostFunctionValue")) + // } + //}() + + // NOTE: check if controller is already deleted + v.checkDeleted() + + switch name { + case sema.StorageCapabilityControllerTypeTagFieldName: + return v.GetTag(config) + + case sema.StorageCapabilityControllerTypeCapabilityIDFieldName: + return v.CapabilityID + + case sema.StorageCapabilityControllerTypeBorrowTypeFieldName: + panic(errors.NewUnreachableError()) + // TODO: + //return NewTypeValue(inter, v.BorrowType) + + case sema.StorageCapabilityControllerTypeCapabilityFieldName: + return v.GetCapability(config) + + } + + return nil +} + +func init() { + typeName := sema.StorageCapabilityControllerType.QualifiedName + + // Capability.borrow + RegisterTypeBoundFunction( + typeName, + sema.StorageCapabilityControllerTypeSetTagFunctionName, + NativeFunctionValue{ + ParameterCount: 0, + Function: func(config *Config, typeArguments []StaticType, args ...Value) Value { + capabilityValue := args[0].(*StorageCapabilityControllerValue) + + //stdlib.SetCapabilityControllerTag(config.interpreter()) + + capabilityValue.checkDeleted() + + return Nil + }, + }) +} + +func (v *StorageCapabilityControllerValue) SetMember( + conf *Config, + name string, + value Value, +) { + // NOTE: check if controller is already deleted + v.checkDeleted() + + switch name { + case sema.StorageCapabilityControllerTypeTagFieldName: + stringValue, ok := value.(*StringValue) + if !ok { + panic(errors.NewUnreachableError()) + } + v.SetTag(conf, stringValue) + } + + panic(errors.NewUnreachableError()) +} + +func (v *StorageCapabilityControllerValue) ControllerCapabilityID() IntValue { + return v.CapabilityID +} + +func (v *StorageCapabilityControllerValue) ReferenceValue( + capabilityAddress common.Address, + resultBorrowType *interpreter.ReferenceStaticType, +) ReferenceValue { + return NewStorageReferenceValue( + resultBorrowType.Authorization, + capabilityAddress, + v.TargetPath, + resultBorrowType.ReferencedType, + ) +} + +// checkDeleted checks if the controller is deleted, +// and panics if it is. +func (v *StorageCapabilityControllerValue) checkDeleted() { + if v.deleted { + panic(errors.NewDefaultUserError("controller is deleted")) + } +} diff --git a/bbq/vm/value_composite.go b/bbq/vm/value_composite.go new file mode 100644 index 0000000000..5ceeb7d104 --- /dev/null +++ b/bbq/vm/value_composite.go @@ -0,0 +1,474 @@ +/* + * 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 ( + goerrors "errors" + + "github.com/onflow/atree" + + "github.com/onflow/cadence/common" + "github.com/onflow/cadence/errors" + "github.com/onflow/cadence/interpreter" +) + +type CompositeValue struct { + dictionary *atree.OrderedMap + CompositeType *interpreter.CompositeStaticType + Kind common.CompositeKind +} + +var _ Value = &CompositeValue{} +var _ MemberAccessibleValue = &CompositeValue{} +var _ ReferenceTrackedResourceKindedValue = &CompositeValue{} + +func NewCompositeValue( + kind common.CompositeKind, + staticType *interpreter.CompositeStaticType, + storage atree.SlabStorage, +) *CompositeValue { + + // Newly created values are always on stack. + // Need to 'Transfer' if needed to be stored in an account. + address := common.ZeroAddress + + dictionary, err := atree.NewMap( + storage, + + atree.Address(address), + + atree.NewDefaultDigesterBuilder(), + interpreter.NewCompositeTypeInfo( + nil, + staticType.Location, + staticType.QualifiedIdentifier, + kind, + ), + ) + + if err != nil { + panic(errors.NewExternalError(err)) + } + + return &CompositeValue{ + CompositeType: staticType, + dictionary: dictionary, + Kind: kind, + } +} + +func newCompositeValueFromOrderedMap( + dict *atree.OrderedMap, + staticType *interpreter.CompositeStaticType, + kind common.CompositeKind, +) *CompositeValue { + return &CompositeValue{ + dictionary: dict, + CompositeType: staticType, + Kind: kind, + } +} + +func (*CompositeValue) isValue() {} + +func (v *CompositeValue) StaticType(*Config) StaticType { + return v.CompositeType +} + +func (v *CompositeValue) GetMember(config *Config, name string) Value { + return v.GetField(config, name) +} + +func (v *CompositeValue) GetField(config *Config, name string) Value { + storedValue, err := v.dictionary.Get( + interpreter.StringAtreeValueComparator, + interpreter.StringAtreeValueHashInput, + interpreter.StringAtreeValue(name), + ) + if err != nil { + var keyNotFoundError *atree.KeyNotFoundError + if goerrors.As(err, &keyNotFoundError) { + return nil + } + panic(errors.NewExternalError(err)) + } + + return MustConvertStoredValue(config.MemoryGauge, config.Storage, storedValue) +} + +func (v *CompositeValue) SetMember(config *Config, name string, value Value) { + + // TODO: + //address := v.StorageID().Address + //value = value.Transfer( + // interpreter, + // locationRange, + // address, + // true, + // nil, + //) + + interpreterValue := VMValueToInterpreterValue(config, value) + + existingStorable, err := v.dictionary.Set( + interpreter.StringAtreeValueComparator, + interpreter.StringAtreeValueHashInput, + interpreter.NewStringAtreeValue(config.MemoryGauge, name), + interpreterValue, + ) + + if err != nil { + panic(errors.NewExternalError(err)) + } + + if existingStorable != nil { + inter := config.interpreter() + existingValue := interpreter.StoredValue(nil, existingStorable, config.Storage) + + existingValue.DeepRemove(inter, true) // existingValue is standalone because it was overwritten in parent container. + + RemoveReferencedSlab(config.Storage, existingStorable) + } +} + +func (v *CompositeValue) SlabID() atree.SlabID { + return v.dictionary.SlabID() +} + +func (v *CompositeValue) TypeID() common.TypeID { + return v.CompositeType.TypeID +} + +func (v *CompositeValue) IsResourceKinded() bool { + return v.Kind == common.CompositeKindResource +} + +func (v *CompositeValue) String() string { + //TODO implement me + panic("implement me") +} + +func (v *CompositeValue) Transfer( + config *Config, + address atree.Address, + remove bool, + storable atree.Storable, +) Value { + + //baseUse, elementOverhead, dataUse, metaDataUse := common.NewCompositeMemoryUsages(v.dictionary.Count(), 0) + //common.UseMemory(interpreter, baseUse) + //common.UseMemory(interpreter, elementOverhead) + //common.UseMemory(interpreter, dataUse) + //common.UseMemory(interpreter, metaDataUse) + // + //interpreter.ReportComputation(common.ComputationKindTransferCompositeValue, 1) + + //if interpreter.Config.InvalidatedResourceValidationEnabled { + // v.checkInvalidatedResourceUse(locationRange) + //} + + //if interpreter.Config.TracingEnabled { + // startTime := time.Now() + // + // owner := v.GetOwner().String() + // typeID := string(v.TypeID()) + // kind := v.Kind.String() + // + // defer func() { + // interpreter.reportCompositeValueTransferTrace( + // owner, + // typeID, + // kind, + // time.Since(startTime), + // ) + // }() + //} + + dictionary := v.dictionary + + needsStoreTo := v.NeedsStoreTo(address) + isResourceKinded := v.IsResourceKinded() + + if needsStoreTo && v.Kind == common.CompositeKindContract { + panic(interpreter.NonTransferableValueError{ + Value: VMValueToInterpreterValue(config, v), + }) + } + + if needsStoreTo || !isResourceKinded { + iterator, err := v.dictionary.Iterator( + interpreter.StringAtreeValueComparator, + interpreter.StringAtreeValueHashInput, + ) + if err != nil { + panic(errors.NewExternalError(err)) + } + + elementMemoryUse := common.NewAtreeMapPreAllocatedElementsMemoryUsage(v.dictionary.Count(), 0) + common.UseMemory(config.MemoryGauge, elementMemoryUse) + + dictionary, err = atree.NewMapFromBatchData( + config.Storage, + address, + atree.NewDefaultDigesterBuilder(), + v.dictionary.Type(), + interpreter.StringAtreeValueComparator, + interpreter.StringAtreeValueHashInput, + v.dictionary.Seed(), + func() (atree.Value, atree.Value, error) { + + atreeKey, atreeValue, err := iterator.Next() + if err != nil { + return nil, nil, err + } + if atreeKey == nil || atreeValue == nil { + return nil, nil, nil + } + + // NOTE: key is stringAtreeValue + // and does not need to be converted or copied + + value := interpreter.MustConvertStoredValue(config.MemoryGauge, atreeValue) + + // TODO: + vmValue := InterpreterValueToVMValue(config.Storage, value) + vmValue.Transfer(config, address, remove, nil) + + return atreeKey, value, nil + }, + ) + if err != nil { + panic(errors.NewExternalError(err)) + } + + if remove { + err = v.dictionary.PopIterate(func(nameStorable atree.Storable, valueStorable atree.Storable) { + RemoveReferencedSlab(config.Storage, nameStorable) + RemoveReferencedSlab(config.Storage, valueStorable) + }) + if err != nil { + panic(errors.NewExternalError(err)) + } + //interpreter.maybeValidateAtreeValue(v.dictionary) + + RemoveReferencedSlab(config.Storage, storable) + } + } + + var res *CompositeValue + + if isResourceKinded { + // Update the resource in-place, + // and also update all values that are referencing the same value + // (but currently point to an outdated Go instance of the value) + + // If checking of transfers of invalidated resource is enabled, + // then mark the resource as invalidated, by unsetting the backing dictionary. + // This allows raising an error when the resource is attempted + // to be transferred/moved again (see beginning of this function) + + invalidateReferencedResources(config, v) + + v.dictionary = nil + } + + if res == nil { + res = newCompositeValueFromOrderedMap( + dictionary, + v.CompositeType, + v.Kind, + ) + + //res.InjectedFields = v.InjectedFields + //res.ComputedFields = v.ComputedFields + //res.NestedVariables = v.NestedVariables + //res.Functions = v.Functions + //res.Destructor = v.Destructor + //res.Stringer = v.Stringer + //res.isDestroyed = v.isDestroyed + } + + //onResourceOwnerChange := interpreter.Config.OnResourceOwnerChange + // + //if needsStoreTo && + // res.Kind == common.CompositeKindResource && + // onResourceOwnerChange != nil { + // + // onResourceOwnerChange( + // interpreter, + // res, + // common.Address(currentAddress), + // common.Address(address), + // ) + //} + + return res +} + +func (v *CompositeValue) Destroy(*Config) { + + //interpreter.ReportComputation(common.ComputationKindDestroyCompositeValue, 1) + // + //if interpreter.Config.InvalidatedResourceValidationEnabled { + // v.checkInvalidatedResourceUse(locationRange) + //} + // + //storageID := v.StorageID() + // + //if interpreter.Config.TracingEnabled { + // startTime := time.Now() + // + // owner := v.GetOwner().String() + // typeID := string(v.TypeID()) + // kind := v.Kind.String() + // + // defer func() { + // + // interpreter.reportCompositeValueDestroyTrace( + // owner, + // typeID, + // kind, + // time.Since(startTime), + // ) + // }() + //} + + //interpreter = v.getInterpreter(interpreter) + + //// if composite was deserialized, dynamically link in the destructor + //if v.Destructor == nil { + // v.Destructor = interpreter.sharedState.typeCodes.CompositeCodes[v.TypeID()].DestructorFunction + //} + // + //destructor := v.Destructor + // + //if destructor != nil { + // invocation := NewInvocation( + // interpreter, + // v, + // nil, + // nil, + // nil, + // locationRange, + // ) + // + // destructor.invoke(invocation) + //} + + //v.isDestroyed = true + + //if interpreter.Config.InvalidatedResourceValidationEnabled { + // v.dictionary = nil + //} + + //interpreter.updateReferencedResource( + // storageID, + // storageID, + // func(value ReferenceTrackedResourceKindedValue) { + // compositeValue, ok := value.(*CompositeValue) + // if !ok { + // panic(errors.NewUnreachableError()) + // } + // + // compositeValue.isDestroyed = true + // + // if interpreter.Config.InvalidatedResourceValidationEnabled { + // compositeValue.dictionary = nil + // } + // }, + //) +} + +func (v *CompositeValue) NeedsStoreTo(address atree.Address) bool { + return address != v.StorageAddress() +} + +func (v *CompositeValue) StorageAddress() atree.Address { + return v.dictionary.Address() +} + +func (v *CompositeValue) IsReferenceTrackedResourceKindedValue() {} + +func (v *CompositeValue) ValueID() atree.ValueID { + return v.dictionary.ValueID() +} + +func (v *CompositeValue) IsStaleResource() bool { + return v.dictionary == nil && v.IsResourceKinded() +} + +// ForEachField iterates over all field-name field-value pairs of the composite value. +// It does NOT iterate over computed fields and functions! +func (v *CompositeValue) ForEachField( + config *Config, + f func(fieldName string, fieldValue Value) (resume bool), +) { + iterate := func(fn atree.MapEntryIterationFunc) error { + return v.dictionary.Iterate( + interpreter.StringAtreeValueComparator, + interpreter.StringAtreeValueHashInput, + fn, + ) + } + v.forEachField( + config, + iterate, + f, + ) +} + +// ForEachReadOnlyLoadedField iterates over all LOADED field-name field-value pairs of the composite value. +// It does NOT iterate over computed fields and functions! +// DO NOT perform storage mutations in the callback! +func (v *CompositeValue) ForEachReadOnlyLoadedField( + config *Config, + f func(fieldName string, fieldValue Value) (resume bool), +) { + v.forEachField( + config, + v.dictionary.IterateReadOnlyLoadedValues, + f, + ) +} + +func (v *CompositeValue) forEachField( + config *Config, + atreeIterate func(fn atree.MapEntryIterationFunc) error, + f func(fieldName string, fieldValue Value) (resume bool), +) { + err := atreeIterate(func(key atree.Value, atreeValue atree.Value) (resume bool, err error) { + value := MustConvertStoredValue( + config.MemoryGauge, + config.Storage, + atreeValue, + ) + + checkInvalidatedResourceOrResourceReference(value) + + resume = f( + string(key.(interpreter.StringAtreeValue)), + value, + ) + return + }) + + if err != nil { + panic(errors.NewExternalError(err)) + } +} diff --git a/bbq/vm/value_conversions.go b/bbq/vm/value_conversions.go new file mode 100644 index 0000000000..175afb6d41 --- /dev/null +++ b/bbq/vm/value_conversions.go @@ -0,0 +1,190 @@ +/* + * 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/common" + "github.com/onflow/cadence/errors" + "github.com/onflow/cadence/interpreter" +) + +// Utility methods to convert between old and new values. +// These are temporary until all parts of the interpreter are migrated to the vm. + +func InterpreterValueToVMValue(storage interpreter.Storage, value interpreter.Value) Value { + switch value := value.(type) { + case nil: + return nil + case interpreter.NilValue: + return Nil + case interpreter.IntValue: + return NewIntValue(value.BigInt.Int64()) + case *interpreter.StringValue: + return NewStringValue(value.Str) + case *interpreter.CompositeValue: + return newCompositeValueFromOrderedMap( + value.AtreeMap(), + value.StaticType(nil).(*interpreter.CompositeStaticType), + value.Kind, + ) + //case interpreter.LinkValue: + // return NewLinkValue( + // InterpreterValueToVMValue(value.TargetPath).(PathValue), + // value.Type, + // ) + case interpreter.PathValue: + return PathValue{ + Domain: value.Domain, + Identifier: value.Identifier, + } + case interpreter.AddressValue: + return AddressValue(value) + case *interpreter.SimpleCompositeValue: + fields := make(map[string]Value) + for name, field := range value.Fields { //nolint:maprange + fields[name] = InterpreterValueToVMValue(storage, field) + } + return NewSimpleCompositeValue( + common.CompositeKindStructure, + value.TypeID, + fields, + ) + case *interpreter.DictionaryValue: + return newDictionaryValueFromAtreeMap( + value.Type, + value.ElementSize(), + value.AtreeMap(), + ) + case *interpreter.IDCapabilityValue: + return NewCapabilityValue( + AddressValue(value.Address()), + NewIntValue(int64(value.ID.ToInt(interpreter.EmptyLocationRange))), + value.BorrowType, + ) + case *interpreter.StorageCapabilityControllerValue: + return NewStorageCapabilityControllerValue( + value.BorrowType, + NewIntValue(int64(value.CapabilityID.ToInt(interpreter.EmptyLocationRange))), + InterpreterValueToVMValue(storage, value.TargetPath).(PathValue), + ) + case *interpreter.StorageReferenceValue: + return NewStorageReferenceValue( + value.Authorization, + value.TargetStorageAddress, + InterpreterValueToVMValue(storage, value.TargetPath).(PathValue), + interpreter.ConvertSemaToStaticType(nil, value.BorrowedType), + ) + default: + panic(errors.NewUnreachableError()) + } +} + +func VMValueToInterpreterValue(config *Config, value Value) interpreter.Value { + switch value := value.(type) { + case nil: + return nil + case NilValue: + return interpreter.Nil + case IntValue: + return interpreter.NewIntValueFromInt64(nil, value.SmallInt) + case StringValue: + return interpreter.NewUnmeteredStringValue(string(value.Str)) + case *CompositeValue: + compositeType := value.CompositeType + return interpreter.NewCompositeValueFromAtreeMap( + nil, + interpreter.CompositeTypeInfo{ + Location: compositeType.Location, + QualifiedIdentifier: compositeType.QualifiedIdentifier, + Kind: value.Kind, + }, + value.dictionary, + ) + case *DictionaryValue: + staticType := value.Type + elementSize := interpreter.DictionaryElementSize(staticType) + return interpreter.NewDictionaryValueFromAtreeMap( + nil, + staticType, + elementSize, + value.dictionary, + ) + case CapabilityValue: + return interpreter.NewCapabilityValue( + nil, + interpreter.NewUnmeteredUInt64Value(uint64(value.ID.SmallInt)), // TODO: properly convert + VMValueToInterpreterValue(config, value.Address).(interpreter.AddressValue), + value.BorrowType, + ) + //case LinkValue: + // return interpreter.LinkValue{ + // TargetPath: VMValueToInterpreterValue(storage, value.TargetPath).(interpreter.PathValue), + // Type: value.StaticType(nil), + // } + case AddressValue: + return interpreter.AddressValue(value) + case PathValue: + return interpreter.PathValue{ + Domain: value.Domain, + Identifier: value.Identifier, + } + case *SimpleCompositeValue: + fields := make(map[string]interpreter.Value) + var fieldNames []string + + // TODO: Fields names order matters. However, this is temporary. So ignore for now. + for name, field := range value.fields { //nolint:maprange + fields[name] = VMValueToInterpreterValue(config, field) + fieldNames = append(fieldNames, name) + } + + return interpreter.NewSimpleCompositeValue( + nil, + value.typeID, + value.staticType, + fieldNames, + fields, + nil, + nil, + nil, + ) + case *StorageReferenceValue: + inter := config.interpreter() + semaBorrowType, err := inter.ConvertStaticToSemaType(value.BorrowedType) + if err != nil { + panic(err) + } + return interpreter.NewStorageReferenceValue( + nil, + value.Authorization, + value.TargetStorageAddress, + VMValueToInterpreterValue(config, value.TargetPath).(interpreter.PathValue), + semaBorrowType, + ) + case *StorageCapabilityControllerValue: + return interpreter.NewStorageCapabilityControllerValue( + nil, + value.BorrowType, + interpreter.NewUnmeteredUInt64Value(uint64(value.CapabilityID.SmallInt)), + VMValueToInterpreterValue(config, value.TargetPath).(interpreter.PathValue), + ) + default: + panic(errors.NewUnreachableError()) + } +} diff --git a/bbq/vm/value_dictionary.go b/bbq/vm/value_dictionary.go new file mode 100644 index 0000000000..83dc1924ef --- /dev/null +++ b/bbq/vm/value_dictionary.go @@ -0,0 +1,409 @@ +/* + * 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/atree" + + "github.com/onflow/cadence/common" + "github.com/onflow/cadence/errors" + "github.com/onflow/cadence/interpreter" +) + +type DictionaryValue struct { + dictionary *atree.OrderedMap + Type *interpreter.DictionaryStaticType + elementSize uint +} + +var _ Value = &DictionaryValue{} +var _ MemberAccessibleValue = &DictionaryValue{} +var _ ReferenceTrackedResourceKindedValue = &DictionaryValue{} + +func NewDictionaryValue( + config *Config, + dictionaryType *interpreter.DictionaryStaticType, + keysAndValues ...Value, +) *DictionaryValue { + + address := common.ZeroAddress + + keysAndValuesCount := len(keysAndValues) + if keysAndValuesCount%2 != 0 { + panic("uneven number of keys and values") + } + + constructor := func() *atree.OrderedMap { + dictionary, err := atree.NewMap( + config.Storage, + atree.Address(address), + atree.NewDefaultDigesterBuilder(), + dictionaryType, + ) + if err != nil { + panic(errors.NewExternalError(err)) + } + return dictionary + } + + // values are added to the dictionary after creation, not here + v := newDictionaryValueFromConstructor(dictionaryType, constructor) + + for i := 0; i < keysAndValuesCount; i += 2 { + key := keysAndValues[i] + value := keysAndValues[i+1] + existingValue := v.Insert(config, key, value) + // If the dictionary already contained a value for the key, + // and the dictionary is resource-typed, + // then we need to prevent a resource loss + if _, ok := existingValue.(*SomeValue); ok { + if v.IsResourceKinded() { + panic(interpreter.DuplicateKeyInResourceDictionaryError{}) + } + } + } + + return v +} + +func newDictionaryValueFromConstructor( + staticType *interpreter.DictionaryStaticType, + constructor func() *atree.OrderedMap, +) *DictionaryValue { + + elementSize := interpreter.DictionaryElementSize(staticType) + return newDictionaryValueFromAtreeMap( + staticType, + elementSize, + constructor(), + ) +} + +func newDictionaryValueFromAtreeMap( + staticType *interpreter.DictionaryStaticType, + elementSize uint, + atreeOrderedMap *atree.OrderedMap, +) *DictionaryValue { + + return &DictionaryValue{ + Type: staticType, + dictionary: atreeOrderedMap, + elementSize: elementSize, + } +} + +func (*DictionaryValue) isValue() {} + +func (v *DictionaryValue) StaticType(*Config) StaticType { + return v.Type +} + +func (v *DictionaryValue) GetMember(config *Config, name string) Value { + // TODO: + return nil +} + +func (v *DictionaryValue) SetMember(conf *Config, name string, value Value) { + // Dictionaries have no settable members (fields / functions) + panic(errors.NewUnreachableError()) +} + +func (v *DictionaryValue) Insert( + conf *Config, + keyValue, value Value, +) Value /* TODO: OptionalValue*/ { + + address := v.dictionary.Address() + + keyValue = keyValue.Transfer( + conf, + address, + true, + nil, + ) + + value = value.Transfer( + conf, + address, + true, + nil, + ) + + //interpreter.checkContainerMutation(v.Type.KeyType, keyValue, locationRange) + //interpreter.checkContainerMutation(v.Type.ValueType, value, locationRange) + + existingValueStorable := v.InsertWithoutTransfer(conf, keyValue, value) + + if existingValueStorable == nil { + return Nil // TODO: NilOptionalValue + } + + existingValue := StoredValue( + conf, + existingValueStorable, + conf.Storage, + ).Transfer( + conf, + atree.Address{}, + true, + existingValueStorable, + ) + + return NewSomeValueNonCopying(existingValue) +} + +func (v *DictionaryValue) InsertWithoutTransfer(config *Config, key, value Value) (existingValueStorable atree.Storable) { + + //interpreter.validateMutation(v.StorageID(), locationRange) + + valueComparator := newValueComparator(config) + hashInputProvider := newHashInputProvider(config) + + keyInterpreterValue := VMValueToInterpreterValue(config, key) + valueInterpreterValue := VMValueToInterpreterValue(config, value) + + // atree only calls Storable() on keyValue if needed, + // i.e., if the key is a new key + var err error + existingValueStorable, err = v.dictionary.Set( + valueComparator, + hashInputProvider, + keyInterpreterValue, + valueInterpreterValue, + ) + if err != nil { + panic(errors.NewExternalError(err)) + } + + //interpreter.maybeValidateAtreeValue(v.dictionary) + + return existingValueStorable +} + +func (v *DictionaryValue) SlabID() atree.SlabID { + return v.dictionary.SlabID() +} + +func (v *DictionaryValue) IsResourceKinded() bool { + // TODO: + return false +} + +func (v *DictionaryValue) String() string { + //TODO implement me + panic("implement me") +} + +func (v *DictionaryValue) Transfer( + config *Config, + address atree.Address, + remove bool, + storable atree.Storable, +) Value { + storage := config.Storage + dictionary := v.dictionary + + needsStoreTo := v.NeedsStoreTo(address) + isResourceKinded := v.IsResourceKinded() + + if needsStoreTo || !isResourceKinded { + valueComparator := newValueComparator(config) + hashInputProvider := newHashInputProvider(config) + + // Use non-readonly iterator here because iterated + // value can be removed if remove parameter is true. + iterator, err := v.dictionary.Iterator(valueComparator, hashInputProvider) + if err != nil { + panic(errors.NewExternalError(err)) + } + + dictionary, err = atree.NewMapFromBatchData( + storage, + address, + atree.NewDefaultDigesterBuilder(), + v.dictionary.Type(), + valueComparator, + hashInputProvider, + v.dictionary.Seed(), + func() (atree.Value, atree.Value, error) { + + atreeKey, atreeValue, err := iterator.Next() + if err != nil { + return nil, nil, err + } + if atreeKey == nil || atreeValue == nil { + return nil, nil, nil + } + + key := interpreter.MustConvertStoredValue(config.MemoryGauge, atreeValue) + value := interpreter.MustConvertStoredValue(config.MemoryGauge, atreeValue) + + // TODO: Transfer both key and value before returning. + return key, value, nil + }, + ) + if err != nil { + panic(errors.NewExternalError(err)) + } + + if remove { + err = v.dictionary.PopIterate(func(keyStorable atree.Storable, valueStorable atree.Storable) { + RemoveReferencedSlab(storage, keyStorable) + RemoveReferencedSlab(storage, valueStorable) + }) + if err != nil { + panic(errors.NewExternalError(err)) + } + //interpreter.maybeValidateAtreeValue(v.dictionary) + + RemoveReferencedSlab(storage, storable) + } + } + + if isResourceKinded { + // Update the resource in-place, + // and also update all values that are referencing the same value + // (but currently point to an outdated Go instance of the value) + + // If checking of transfers of invalidated resource is enabled, + // then mark the resource array as invalidated, by unsetting the backing array. + // This allows raising an error when the resource array is attempted + // to be transferred/moved again (see beginning of this function) + + invalidateReferencedResources(config, v) + + v.dictionary = nil + } + + res := newDictionaryValueFromAtreeMap( + v.Type, + v.elementSize, + dictionary, + ) + + //res.semaType = v.semaType + //res.isResourceKinded = v.isResourceKinded + //res.isDestroyed = v.isDestroyed + + return res +} + +func (v *DictionaryValue) Destroy(*Config) { + v.dictionary = nil +} + +func (v *DictionaryValue) NeedsStoreTo(address atree.Address) bool { + return address != v.StorageAddress() +} + +func (v *DictionaryValue) StorageAddress() atree.Address { + return v.dictionary.Address() +} + +func (v *DictionaryValue) IsReferenceTrackedResourceKindedValue() {} + +func (v *DictionaryValue) ValueID() atree.ValueID { + return v.dictionary.ValueID() +} + +func (v *DictionaryValue) IsStaleResource() bool { + return v.dictionary == nil && v.IsResourceKinded() +} + +func (v *DictionaryValue) Iterate( + config *Config, + f func(key, value Value) (resume bool), +) { + valueComparator := newValueComparator(config) + hashInputProvider := newHashInputProvider(config) + iterate := func(fn atree.MapEntryIterationFunc) error { + return v.dictionary.Iterate( + valueComparator, + hashInputProvider, + fn, + ) + } + v.iterate(config, iterate, f) +} + +// IterateReadOnlyLoaded iterates over all LOADED key-valye pairs of the array. +// DO NOT perform storage mutations in the callback! +func (v *DictionaryValue) IterateReadOnlyLoaded( + config *Config, + f func(key, value Value) (resume bool), +) { + v.iterate( + config, + v.dictionary.IterateReadOnlyLoadedValues, + f, + ) +} + +func (v *DictionaryValue) iterate( + config *Config, + atreeIterate func(fn atree.MapEntryIterationFunc) error, + f func(key Value, value Value) (resume bool), +) { + iterate := func() { + err := atreeIterate(func(key, value atree.Value) (resume bool, err error) { + // atree.OrderedMap iteration provides low-level atree.Value, + // convert to high-level interpreter.Value + + keyValue := MustConvertStoredValue(config.MemoryGauge, config.Storage, key) + valueValue := MustConvertStoredValue(config.MemoryGauge, config.Storage, value) + + checkInvalidatedResourceOrResourceReference(keyValue) + checkInvalidatedResourceOrResourceReference(valueValue) + + resume = f( + keyValue, + valueValue, + ) + + return resume, nil + }) + if err != nil { + panic(errors.NewExternalError(err)) + } + } + + // TODO: + //interpreter.withMutationPrevention(v.ValueID(), iterate) + iterate() +} + +func newValueComparator(conf *Config) atree.ValueComparator { + return func(storage atree.SlabStorage, atreeValue atree.Value, otherStorable atree.Storable) (bool, error) { + inter := conf.interpreter() + locationRange := interpreter.EmptyLocationRange + value := interpreter.MustConvertStoredValue(inter, atreeValue) + otherValue := interpreter.StoredValue(inter, otherStorable, storage) + return value.(interpreter.EquatableValue).Equal(inter, locationRange, otherValue), nil + } +} + +func newHashInputProvider(conf *Config) atree.HashInputProvider { + return func(value atree.Value, scratch []byte) ([]byte, error) { + inter := conf.interpreter() + locationRange := interpreter.EmptyLocationRange + hashInput := interpreter.MustConvertStoredValue(inter, value).(interpreter.HashableValue). + HashInput(inter, locationRange, scratch) + return hashInput, nil + } +} diff --git a/bbq/vm/value_ephemeral_reference.go b/bbq/vm/value_ephemeral_reference.go new file mode 100644 index 0000000000..d7bd542554 --- /dev/null +++ b/bbq/vm/value_ephemeral_reference.go @@ -0,0 +1,99 @@ +/* + * 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/atree" + "github.com/onflow/cadence/format" + "github.com/onflow/cadence/interpreter" +) + +type ReferenceValue interface { + Value + //AuthorizedValue + isReference() + ReferencedValue(config *Config, errorOnFailedDereference bool) *Value + BorrowType() interpreter.StaticType +} + +type EphemeralReferenceValue struct { + Value Value + // BorrowedType is the T in &T + BorrowedType interpreter.StaticType + Authorization interpreter.Authorization +} + +var _ Value = &EphemeralReferenceValue{} +var _ MemberAccessibleValue = &EphemeralReferenceValue{} +var _ ReferenceValue = &EphemeralReferenceValue{} + +func NewEphemeralReferenceValue( + conf *Config, + value Value, + authorization interpreter.Authorization, + borrowedType interpreter.StaticType, +) *EphemeralReferenceValue { + ref := &EphemeralReferenceValue{ + Value: value, + Authorization: authorization, + BorrowedType: borrowedType, + } + + maybeTrackReferencedResourceKindedValue(conf, ref) + + return ref +} + +func (*EphemeralReferenceValue) isValue() {} + +func (v *EphemeralReferenceValue) isReference() {} + +func (v *EphemeralReferenceValue) ReferencedValue(*Config, bool) *Value { + return &v.Value +} + +func (v *EphemeralReferenceValue) BorrowType() interpreter.StaticType { + return v.BorrowedType +} + +func (v *EphemeralReferenceValue) StaticType(config *Config) StaticType { + return interpreter.NewReferenceStaticType( + config.MemoryGauge, + v.Authorization, + v.Value.StaticType(config), + ) +} + +func (v *EphemeralReferenceValue) Transfer(*Config, atree.Address, bool, atree.Storable) Value { + return v +} + +func (v *EphemeralReferenceValue) String() string { + return format.StorageReference +} + +func (v *EphemeralReferenceValue) GetMember(config *Config, name string) Value { + memberAccessibleValue := v.Value.(MemberAccessibleValue) + return memberAccessibleValue.GetMember(config, name) +} + +func (v *EphemeralReferenceValue) SetMember(config *Config, name string, value Value) { + memberAccessibleValue := v.Value.(MemberAccessibleValue) + memberAccessibleValue.SetMember(config, name, value) +} diff --git a/bbq/vm/value_function.go b/bbq/vm/value_function.go new file mode 100644 index 0000000000..1a41cd09db --- /dev/null +++ b/bbq/vm/value_function.go @@ -0,0 +1,74 @@ +/* + * 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/atree" + + "github.com/onflow/cadence/bbq" + "github.com/onflow/cadence/bbq/opcode" + "github.com/onflow/cadence/errors" +) + +type FunctionValue struct { + Function *bbq.Function[opcode.Instruction] + Executable *ExecutableProgram +} + +var _ Value = FunctionValue{} + +func (FunctionValue) isValue() {} + +func (FunctionValue) StaticType(*Config) StaticType { + panic(errors.NewUnreachableError()) +} + +func (v FunctionValue) Transfer(*Config, atree.Address, bool, atree.Storable) Value { + return v +} + +func (v FunctionValue) String() string { + //TODO implement me + panic("implement me") +} + +type NativeFunction func(config *Config, typeArguments []StaticType, arguments ...Value) Value + +type NativeFunctionValue struct { + Name string + ParameterCount int + Function NativeFunction +} + +var _ Value = NativeFunctionValue{} + +func (NativeFunctionValue) isValue() {} + +func (NativeFunctionValue) StaticType(*Config) StaticType { + panic(errors.NewUnreachableError()) +} + +func (v NativeFunctionValue) Transfer(*Config, atree.Address, bool, atree.Storable) Value { + return v +} + +func (v NativeFunctionValue) String() string { + //TODO implement me + panic("implement me") +} diff --git a/bbq/vm/value_int.go b/bbq/vm/value_int.go new file mode 100644 index 0000000000..42065feff4 --- /dev/null +++ b/bbq/vm/value_int.go @@ -0,0 +1,76 @@ +/* + * 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 ( + "strconv" + + "github.com/onflow/atree" + + "github.com/onflow/cadence/interpreter" +) + +type IntValue struct { + SmallInt int64 +} + +func NewIntValue(smallInt int64) IntValue { + return IntValue{ + SmallInt: smallInt, + } +} + +func (v IntValue) String() string { + return strconv.FormatInt(v.SmallInt, 10) +} + +var _ Value = IntValue{} + +func (IntValue) isValue() {} + +func (IntValue) StaticType(*Config) StaticType { + return interpreter.PrimitiveStaticTypeInt +} + +func (v IntValue) Transfer(*Config, atree.Address, bool, atree.Storable) Value { + return v +} + +func (v IntValue) Add(other IntValue) Value { + sum := safeAdd(int(v.SmallInt), int(other.SmallInt)) + return NewIntValue(int64(sum)) +} + +func (v IntValue) Subtract(other IntValue) Value { + return NewIntValue(v.SmallInt - other.SmallInt) +} + +func (v IntValue) Less(other IntValue) Value { + if v.SmallInt < other.SmallInt { + return TrueValue + } + return FalseValue +} + +func (v IntValue) Greater(other IntValue) Value { + if v.SmallInt > other.SmallInt { + return TrueValue + } + return FalseValue +} diff --git a/bbq/vm/value_nil.go b/bbq/vm/value_nil.go new file mode 100644 index 0000000000..f4d87d30c9 --- /dev/null +++ b/bbq/vm/value_nil.go @@ -0,0 +1,46 @@ +/* + * 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/atree" + "github.com/onflow/cadence/format" + "github.com/onflow/cadence/interpreter" +) + +type NilValue struct{} + +var Nil Value = NilValue{} + +func (NilValue) isValue() {} + +func (NilValue) StaticType(config *Config) StaticType { + return interpreter.NewOptionalStaticType( + config.MemoryGauge, + interpreter.PrimitiveStaticTypeNever, + ) +} + +func (v NilValue) Transfer(*Config, atree.Address, bool, atree.Storable) Value { + return v +} + +func (v NilValue) String() string { + return format.Nil +} diff --git a/bbq/vm/value_path.go b/bbq/vm/value_path.go new file mode 100644 index 0000000000..42f22ca3f9 --- /dev/null +++ b/bbq/vm/value_path.go @@ -0,0 +1,63 @@ +/* + * 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/atree" + + "github.com/onflow/cadence/common" + "github.com/onflow/cadence/errors" + "github.com/onflow/cadence/format" + "github.com/onflow/cadence/interpreter" +) + +type PathValue struct { + Domain common.PathDomain + Identifier string +} + +var EmptyPathValue = PathValue{} + +var _ Value = PathValue{} + +func (PathValue) isValue() {} + +func (v PathValue) StaticType(*Config) StaticType { + switch v.Domain { + case common.PathDomainStorage: + return interpreter.PrimitiveStaticTypeStoragePath + case common.PathDomainPublic: + return interpreter.PrimitiveStaticTypePublicPath + case common.PathDomainPrivate: + return interpreter.PrimitiveStaticTypePrivatePath + default: + panic(errors.NewUnreachableError()) + } +} + +func (v PathValue) Transfer(*Config, atree.Address, bool, atree.Storable) Value { + return v +} + +func (v PathValue) String() string { + return format.Path( + v.Domain.Identifier(), + v.Identifier, + ) +} diff --git a/bbq/vm/value_simple_composite.go b/bbq/vm/value_simple_composite.go new file mode 100644 index 0000000000..ad1803546a --- /dev/null +++ b/bbq/vm/value_simple_composite.go @@ -0,0 +1,92 @@ +/* + * 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/atree" + "github.com/onflow/cadence/common" +) + +type SimpleCompositeValue struct { + typeID common.TypeID + staticType StaticType + Kind common.CompositeKind + + fields map[string]Value + computeField func(name string) Value + + // metadata is a property bag to carry internal data + // that are not visible to cadence users. + // TODO: any better way to pass down information? + metadata map[string]any +} + +var _ Value = &CompositeValue{} + +func NewSimpleCompositeValue( + kind common.CompositeKind, + typeID common.TypeID, + fields map[string]Value, +) *SimpleCompositeValue { + + return &SimpleCompositeValue{ + Kind: kind, + typeID: typeID, + fields: fields, + } +} + +func (*SimpleCompositeValue) isValue() {} + +func (v *SimpleCompositeValue) StaticType(*Config) StaticType { + return v.staticType +} + +func (v *SimpleCompositeValue) GetMember(_ *Config, name string) Value { + value, ok := v.fields[name] + if ok { + return value + } + + return v.computeField(name) +} + +func (v *SimpleCompositeValue) SetMember(_ *Config, name string, value Value) { + v.fields[name] = value +} + +func (v *SimpleCompositeValue) IsResourceKinded() bool { + return v.Kind == common.CompositeKindResource +} + +func (v *SimpleCompositeValue) String() string { + //TODO implement me + panic("implement me") +} + +func (v *SimpleCompositeValue) Transfer( + conf *Config, + address atree.Address, + remove bool, + storable atree.Storable, +) Value { + return v +} + +func (v *SimpleCompositeValue) Destroy(*Config) {} diff --git a/bbq/vm/value_some.go b/bbq/vm/value_some.go new file mode 100644 index 0000000000..041d3bc80b --- /dev/null +++ b/bbq/vm/value_some.go @@ -0,0 +1,74 @@ +/* + * 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/atree" + + "github.com/onflow/cadence/interpreter" +) + +type SomeValue struct { + value Value +} + +var _ Value = &SomeValue{} +var _ MemberAccessibleValue = &SomeValue{} +var _ ResourceKindedValue = &SomeValue{} + +func NewSomeValueNonCopying(value Value) *SomeValue { + return &SomeValue{ + value: value, + } +} + +func (*SomeValue) isValue() {} + +func (v *SomeValue) StaticType(config *Config) StaticType { + innerType := v.value.StaticType(config) + if innerType == nil { + return nil + } + return interpreter.NewOptionalStaticType( + config, + innerType, + ) +} + +func (v *SomeValue) Transfer(*Config, atree.Address, bool, atree.Storable) Value { + return v +} + +func (v *SomeValue) String() string { + return v.value.String() +} +func (v *SomeValue) GetMember(config *Config, name string) Value { + memberAccessibleValue := (v.value).(MemberAccessibleValue) + return memberAccessibleValue.GetMember(config, name) +} + +func (v *SomeValue) SetMember(config *Config, name string, value Value) { + memberAccessibleValue := (v.value).(MemberAccessibleValue) + memberAccessibleValue.SetMember(config, name, value) +} + +func (v *SomeValue) IsResourceKinded() bool { + resourceKinded, ok := v.value.(ResourceKindedValue) + return ok && resourceKinded.IsResourceKinded() +} diff --git a/bbq/vm/value_storage_reference.go b/bbq/vm/value_storage_reference.go new file mode 100644 index 0000000000..38158f2345 --- /dev/null +++ b/bbq/vm/value_storage_reference.go @@ -0,0 +1,145 @@ +/* + * 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 ( + "fmt" + "github.com/onflow/atree" + "github.com/onflow/cadence/common" + "github.com/onflow/cadence/format" + "github.com/onflow/cadence/interpreter" +) + +type StorageReferenceValue struct { + Authorization interpreter.Authorization + TargetStorageAddress common.Address + TargetPath PathValue + BorrowedType interpreter.StaticType +} + +var _ Value = &StorageReferenceValue{} +var _ MemberAccessibleValue = &StorageReferenceValue{} +var _ ReferenceValue = &StorageReferenceValue{} + +func NewStorageReferenceValue( + authorization interpreter.Authorization, + targetStorageAddress common.Address, + targetPath PathValue, + borrowedType interpreter.StaticType, +) *StorageReferenceValue { + return &StorageReferenceValue{ + Authorization: authorization, + TargetStorageAddress: targetStorageAddress, + TargetPath: targetPath, + BorrowedType: borrowedType, + } +} + +func (*StorageReferenceValue) isValue() {} + +func (v *StorageReferenceValue) isReference() {} + +func (v *StorageReferenceValue) ReferencedValue(config *Config, errorOnFailedDereference bool) *Value { + referenced, err := v.dereference(config) + if err != nil && errorOnFailedDereference { + panic(err) + } + + return referenced +} + +func (v *StorageReferenceValue) BorrowType() interpreter.StaticType { + return v.BorrowedType +} + +func (v *StorageReferenceValue) StaticType(config *Config) StaticType { + referencedValue, err := v.dereference(config) + if err != nil { + panic(err) + } + + memoryGauge := config.MemoryGauge + + return interpreter.NewReferenceStaticType( + memoryGauge, + v.Authorization, + (*referencedValue).StaticType(config), + ) +} + +func (v *StorageReferenceValue) dereference(config *Config) (*Value, error) { + memoryGauge := config.MemoryGauge + address := v.TargetStorageAddress + domain := v.TargetPath.Domain.Identifier() + identifier := v.TargetPath.Identifier + + vmReferencedValue := ReadStored(memoryGauge, config.Storage, address, domain, identifier) + if vmReferencedValue == nil { + return nil, nil + } + + if _, isReference := vmReferencedValue.(ReferenceValue); isReference { + panic(interpreter.NestedReferenceError{}) + } + + if v.BorrowedType != nil { + staticType := vmReferencedValue.StaticType(config) + + if !IsSubType(config, staticType, v.BorrowedType) { + panic(fmt.Errorf("type mismatch: expected %s, found %s", v.BorrowedType, staticType)) + //semaType := interpreter.MustConvertStaticToSemaType(staticType) + // + //return nil, ForceCastTypeMismatchError{ + // ExpectedType: v.BorrowedType, + // ActualType: semaType, + // LocationRange: locationRange, + //} + } + } + + return &vmReferencedValue, nil +} + +func (v *StorageReferenceValue) Transfer(*Config, atree.Address, bool, atree.Storable) Value { + return v +} + +func (v *StorageReferenceValue) String() string { + return format.StorageReference +} + +func (v *StorageReferenceValue) GetMember(config *Config, name string) Value { + referencedValue, err := v.dereference(config) + if err != nil { + panic(err) + } + + memberAccessibleValue := (*referencedValue).(MemberAccessibleValue) + return memberAccessibleValue.GetMember(config, name) +} + +func (v *StorageReferenceValue) SetMember(config *Config, name string, value Value) { + referencedValue, err := v.dereference(config) + if err != nil { + panic(err) + } + + memberAccessibleValue := (*referencedValue).(MemberAccessibleValue) + memberAccessibleValue.SetMember(config, name, value) +} diff --git a/bbq/vm/value_string.go b/bbq/vm/value_string.go new file mode 100644 index 0000000000..6029d4ec8c --- /dev/null +++ b/bbq/vm/value_string.go @@ -0,0 +1,82 @@ +/* + * 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 ( + "strings" + + "github.com/onflow/atree" + + "github.com/onflow/cadence/interpreter" + "github.com/onflow/cadence/sema" +) + +type StringValue struct { + Str []byte +} + +var _ Value = StringValue{} + +func NewStringValue(str string) StringValue { + return StringValue{ + Str: []byte(str), + } +} + +func NewStringValueFromBytes(bytes []byte) StringValue { + return StringValue{ + Str: bytes, + } +} + +func (StringValue) isValue() {} + +func (StringValue) StaticType(*Config) StaticType { + return interpreter.PrimitiveStaticTypeString +} + +func (v StringValue) Transfer(*Config, atree.Address, bool, atree.Storable) Value { + return v +} + +func (v StringValue) String() string { + return string(v.Str) +} + +// members + +const ( + StringConcatFunctionName = "concat" +) + +func init() { + typeName := interpreter.PrimitiveStaticTypeString.String() + + RegisterTypeBoundFunction(typeName, StringConcatFunctionName, NativeFunctionValue{ + ParameterCount: len(sema.StringTypeConcatFunctionType.Parameters), + Function: func(config *Config, typeArguments []StaticType, value ...Value) Value { + first := value[0].(StringValue) + second := value[1].(StringValue) + var sb strings.Builder + sb.Write(first.Str) + sb.Write(second.Str) + return NewStringValue(sb.String()) + }, + }) +} diff --git a/bbq/vm/value_utils.go b/bbq/vm/value_utils.go new file mode 100644 index 0000000000..89b74a57a4 --- /dev/null +++ b/bbq/vm/value_utils.go @@ -0,0 +1,35 @@ +/* + * 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/interpreter" + +const goIntSize = 32 << (^uint(0) >> 63) // 32 or 64 +const goMaxInt = 1<<(goIntSize-1) - 1 +const goMinInt = -1 << (goIntSize - 1) + +func safeAdd(a, b int) int { + // INT32-C + if (b > 0) && (a > (goMaxInt - b)) { + panic(interpreter.OverflowError{}) + } else if (b < 0) && (a < (goMinInt - b)) { + panic(interpreter.UnderflowError{}) + } + return a + b +} diff --git a/bbq/vm/value_void.go b/bbq/vm/value_void.go new file mode 100644 index 0000000000..a845ca3515 --- /dev/null +++ b/bbq/vm/value_void.go @@ -0,0 +1,44 @@ +/* + * 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/atree" + "github.com/onflow/cadence/format" + + "github.com/onflow/cadence/interpreter" +) + +type VoidValue struct{} + +var Void Value = VoidValue{} + +func (VoidValue) isValue() {} + +func (VoidValue) StaticType(*Config) StaticType { + return interpreter.PrimitiveStaticTypeVoid +} + +func (v VoidValue) Transfer(*Config, atree.Address, bool, atree.Storable) Value { + return v +} + +func (v VoidValue) String() string { + return format.Void +} diff --git a/bbq/vm/vm.go b/bbq/vm/vm.go new file mode 100644 index 0000000000..39ca5ce124 --- /dev/null +++ b/bbq/vm/vm.go @@ -0,0 +1,822 @@ +/* + * 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/atree" + + "github.com/onflow/cadence/bbq" + "github.com/onflow/cadence/bbq/commons" + "github.com/onflow/cadence/bbq/constantkind" + "github.com/onflow/cadence/bbq/leb128" + "github.com/onflow/cadence/bbq/opcode" + "github.com/onflow/cadence/common" + "github.com/onflow/cadence/errors" + "github.com/onflow/cadence/interpreter" + "github.com/onflow/cadence/runtime" +) + +type VM struct { + stack []Value + locals []Value + + callstack []callFrame + callFrame callFrame + + ipStack []uint16 + ip uint16 + + config *Config + globals map[string]Value + linkedGlobalsCache map[common.Location]LinkedGlobals +} + +func NewVM( + location common.Location, + program *bbq.Program[opcode.Instruction], + conf *Config, +) *VM { + // TODO: Remove initializing config. Following is for testing purpose only. + if conf == nil { + conf = &Config{} + } + if conf.Storage == nil { + conf.Storage = interpreter.NewInMemoryStorage(nil) + } + + // linkedGlobalsCache is a local cache-alike that is being used to hold already linked imports. + linkedGlobalsCache := map[common.Location]LinkedGlobals{ + BuiltInLocation: { + // It is NOT safe to re-use native functions map here because, + // once put into the cache, it will be updated by adding the + // globals of the current program. + indexedGlobals: NativeFunctions(), + }, + } + + vm := &VM{ + linkedGlobalsCache: linkedGlobalsCache, + config: conf, + } + + // Link global variables and functions. + linkedGlobals := LinkGlobals( + location, + program, + conf, + linkedGlobalsCache, + ) + + vm.globals = linkedGlobals.indexedGlobals + + return vm +} + +func (vm *VM) push(value Value) { + vm.stack = append(vm.stack, value) +} + +func (vm *VM) pop() Value { + lastIndex := len(vm.stack) - 1 + value := vm.stack[lastIndex] + vm.stack[lastIndex] = nil + vm.stack = vm.stack[:lastIndex] + + checkInvalidatedResourceOrResourceReference(value) + + return value +} + +// pop2 removes and returns the top two value of the stack. +// It is efficient than calling `pop` twice. +func (vm *VM) pop2() (Value, Value) { + lastIndex := len(vm.stack) - 1 + value1, value2 := vm.stack[lastIndex], vm.stack[lastIndex-1] + vm.stack[lastIndex], vm.stack[lastIndex-1] = nil, nil + vm.stack = vm.stack[:lastIndex-1] + return value1, value2 +} + +// pop3 removes and returns the top three value of the stack. +// It is efficient than calling `pop` thrice. +func (vm *VM) pop3() (Value, Value, Value) { + lastIndex := len(vm.stack) - 1 + value1, value2, value3 := vm.stack[lastIndex], vm.stack[lastIndex-1], vm.stack[lastIndex-2] + vm.stack[lastIndex], vm.stack[lastIndex-1], vm.stack[lastIndex-2] = nil, nil, nil + vm.stack = vm.stack[:lastIndex-2] + return value1, value2, value3 +} + +func (vm *VM) peek() Value { + lastIndex := len(vm.stack) - 1 + return vm.stack[lastIndex] +} + +func (vm *VM) dropN(count int) { + stackHeight := len(vm.stack) + for i := 1; i <= count; i++ { + vm.stack[stackHeight-i] = nil + } + vm.stack = vm.stack[:stackHeight-count] +} + +func (vm *VM) peekPop() (Value, Value) { + lastIndex := len(vm.stack) - 1 + return vm.stack[lastIndex-1], vm.pop() +} + +func (vm *VM) replaceTop(value Value) { + lastIndex := len(vm.stack) - 1 + vm.stack[lastIndex] = value +} + +func fill(slice []Value, n int) []Value { + for i := 0; i < n; i++ { + slice = append(slice, nil) + } + return slice +} + +func (vm *VM) pushCallFrame(functionValue FunctionValue, arguments []Value) { + localsCount := functionValue.Function.LocalCount + + vm.locals = append(vm.locals, arguments...) + vm.locals = fill(vm.locals, int(localsCount)-len(arguments)) + + // Calculate the offset for local variable for the new callframe. + // This is equal to: (local var offset + local var count) of previous callframe. + var offset uint16 + if len(vm.callstack) > 0 { + offset = vm.callFrame.localsOffset + vm.callFrame.localsCount + + // store/update the current ip, so it can be resumed. + vm.ipStack[len(vm.ipStack)-1] = vm.ip + } + + callFrame := callFrame{ + localsCount: localsCount, + localsOffset: offset, + function: functionValue.Function, + executable: functionValue.Executable, + } + + vm.ipStack = append(vm.ipStack, 0) + vm.ip = 0 + + vm.callstack = append(vm.callstack, callFrame) + vm.callFrame = callFrame +} + +func (vm *VM) popCallFrame() { + vm.locals = vm.locals[:vm.callFrame.localsOffset] + + newIpStackDepth := len(vm.ipStack) - 1 + vm.ipStack = vm.ipStack[:newIpStackDepth] + + newStackDepth := len(vm.callstack) - 1 + vm.callstack = vm.callstack[:newStackDepth] + + if newStackDepth == 0 { + vm.ip = 0 + } else { + vm.ip = vm.ipStack[newIpStackDepth-1] + vm.callFrame = vm.callstack[newStackDepth-1] + } +} + +func (vm *VM) Invoke(name string, arguments ...Value) (v Value, err error) { + function, ok := vm.globals[name] + if !ok { + return nil, errors.NewDefaultUserError("unknown function '%s'", name) + } + + defer func() { + recovered := recover() + if recovered == nil { + return + } + + // TODO: pass proper location + codesAndPrograms := runtime.NewCodesAndPrograms() + err = runtime.GetWrappedError(recovered, nil, codesAndPrograms) + }() + + return vm.invoke(function, arguments) +} + +func (vm *VM) invoke(function Value, arguments []Value) (Value, error) { + functionValue, ok := function.(FunctionValue) + if !ok { + return nil, errors.NewDefaultUserError("not invocable") + } + + if len(arguments) != int(functionValue.Function.ParameterCount) { + return nil, errors.NewDefaultUserError( + "wrong number of arguments: expected %d, found %d", + functionValue.Function.ParameterCount, + len(arguments), + ) + } + + vm.pushCallFrame(functionValue, arguments) + + vm.run() + + if len(vm.stack) == 0 { + return nil, nil + } + + return vm.pop(), nil +} + +func (vm *VM) InitializeContract(arguments ...Value) (*CompositeValue, error) { + value, err := vm.Invoke(commons.InitFunctionName, arguments...) + if err != nil { + return nil, err + } + + contractValue, ok := value.(*CompositeValue) + if !ok { + return nil, errors.NewUnexpectedError("invalid contract value") + } + + return contractValue, nil +} + +func (vm *VM) ExecuteTransaction(transactionArgs []Value, signers ...Value) error { + // Create transaction value + transaction, err := vm.Invoke(commons.TransactionWrapperCompositeName) + if err != nil { + return err + } + + if initializer, ok := vm.globals[commons.ProgramInitFunctionName]; ok { + _, err = vm.invoke(initializer, transactionArgs) + if err != nil { + return err + } + } + + prepareArgs := make([]Value, 0, len(signers)+1) + prepareArgs = append(prepareArgs, transaction) + prepareArgs = append(prepareArgs, signers...) + + // Invoke 'prepare', if exists. + if prepare, ok := vm.globals[commons.TransactionPrepareFunctionName]; ok { + _, err = vm.invoke(prepare, prepareArgs) + if err != nil { + return err + } + } + + // TODO: Invoke pre/post conditions + + // Invoke 'execute', if exists. + executeArgs := []Value{transaction} + if execute, ok := vm.globals[commons.TransactionExecuteFunctionName]; ok { + _, err = vm.invoke(execute, executeArgs) + return err + } + + return nil +} + +func opReturnValue(vm *VM) { + value := vm.pop() + vm.popCallFrame() + vm.push(value) +} + +var voidValue = VoidValue{} + +func opReturn(vm *VM) { + vm.popCallFrame() + vm.push(voidValue) +} + +func opJump(vm *VM, ins opcode.InstructionJump) { + vm.ip = ins.Target +} + +func opJumpIfFalse(vm *VM, ins opcode.InstructionJumpIfFalse) { + value := vm.pop().(BoolValue) + if !value { + vm.ip = ins.Target + } +} + +func opBinaryIntAdd(vm *VM) { + left, right := vm.peekPop() + leftNumber := left.(IntValue) + rightNumber := right.(IntValue) + vm.replaceTop(leftNumber.Add(rightNumber)) +} + +func opBinaryIntSubtract(vm *VM) { + left, right := vm.peekPop() + leftNumber := left.(IntValue) + rightNumber := right.(IntValue) + vm.replaceTop(leftNumber.Subtract(rightNumber)) +} + +func opBinaryIntLess(vm *VM) { + left, right := vm.peekPop() + leftNumber := left.(IntValue) + rightNumber := right.(IntValue) + vm.replaceTop(leftNumber.Less(rightNumber)) +} + +func opBinaryIntGreater(vm *VM) { + left, right := vm.peekPop() + leftNumber := left.(IntValue) + rightNumber := right.(IntValue) + vm.replaceTop(leftNumber.Greater(rightNumber)) +} + +func opTrue(vm *VM) { + vm.push(TrueValue) +} + +func opFalse(vm *VM) { + vm.push(FalseValue) +} + +func opGetConstant(vm *VM, ins opcode.InstructionGetConstant) { + constant := vm.callFrame.executable.Constants[ins.ConstantIndex] + if constant == nil { + constant = vm.initializeConstant(ins.ConstantIndex) + } + vm.push(constant) +} + +func opGetLocal(vm *VM, ins opcode.InstructionGetLocal) { + absoluteIndex := vm.callFrame.localsOffset + ins.LocalIndex + local := vm.locals[absoluteIndex] + vm.push(local) +} + +func opSetLocal(vm *VM, ins opcode.InstructionSetLocal) { + absoluteIndex := vm.callFrame.localsOffset + ins.LocalIndex + vm.locals[absoluteIndex] = vm.pop() +} + +func opGetGlobal(vm *VM, ins opcode.InstructionGetGlobal) { + value := vm.callFrame.executable.Globals[ins.GlobalIndex] + vm.push(value) +} + +func opSetGlobal(vm *VM, ins opcode.InstructionSetGlobal) { + vm.callFrame.executable.Globals[ins.GlobalIndex] = vm.pop() +} + +func opSetIndex(vm *VM) { + index, array, element := vm.pop3() + indexValue := index.(IntValue) + arrayValue := array.(*ArrayValue) + arrayValue.Set(vm.config, int(indexValue.SmallInt), element) +} + +func opGetIndex(vm *VM) { + index, array := vm.pop2() + indexValue := index.(IntValue) + arrayValue := array.(*ArrayValue) + element := arrayValue.Get(vm.config, int(indexValue.SmallInt)) + vm.push(element) +} + +func opInvoke(vm *VM, ins opcode.InstructionInvoke) { + value := vm.pop() + stackHeight := len(vm.stack) + + switch value := value.(type) { + case FunctionValue: + parameterCount := int(value.Function.ParameterCount) + arguments := vm.stack[stackHeight-parameterCount:] + vm.pushCallFrame(value, arguments) + vm.dropN(parameterCount) + + case NativeFunctionValue: + parameterCount := value.ParameterCount + + var typeArguments []StaticType + for _, index := range ins.TypeArgs { + typeArg := vm.loadType(index) + typeArguments = append(typeArguments, typeArg) + } + + arguments := vm.stack[stackHeight-parameterCount:] + + result := value.Function(vm.config, typeArguments, arguments...) + vm.dropN(parameterCount) + vm.push(result) + + default: + panic(errors.NewUnreachableError()) + } +} + +func opInvokeDynamic(vm *VM, ins opcode.InstructionInvokeDynamic) { + stackHeight := len(vm.stack) + receiver := vm.stack[stackHeight-int(ins.ArgCount)-1] + + // TODO: + var typeArguments []StaticType + for _, index := range ins.TypeArgs { + typeArg := vm.loadType(index) + typeArguments = append(typeArguments, typeArg) + } + // TODO: Just to make the linter happy + _ = typeArguments + + switch typedReceiver := receiver.(type) { + case *StorageReferenceValue: + referenced, err := typedReceiver.dereference(vm.config) + if err != nil { + panic(err) + } + receiver = *referenced + + // TODO: + //case ReferenceValue + } + + compositeValue := receiver.(*CompositeValue) + compositeType := compositeValue.CompositeType + + funcName := getStringConstant(vm, ins.NameIndex) + + qualifiedFuncName := commons.TypeQualifiedName(compositeType.QualifiedIdentifier, funcName) + var functionValue = vm.lookupFunction(compositeType.Location, qualifiedFuncName) + + parameterCount := int(functionValue.Function.ParameterCount) + arguments := vm.stack[stackHeight-parameterCount:] + vm.pushCallFrame(functionValue, arguments) + vm.dropN(parameterCount) +} + +func opDrop(vm *VM) { + _ = vm.pop() +} + +func opDup(vm *VM) { + top := vm.peek() + vm.push(top) +} + +func opNew(vm *VM, ins opcode.InstructionNew) { + compositeKind := ins.Kind + + // decode location + staticType := vm.loadType(ins.TypeIndex) + + // TODO: Support inclusive-range type + compositeStaticType := staticType.(*interpreter.CompositeStaticType) + + value := NewCompositeValue( + compositeKind, + compositeStaticType, + vm.config.Storage, + ) + vm.push(value) +} + +func opSetField(vm *VM, ins opcode.InstructionSetField) { + // TODO: support all container types + structValue := vm.pop().(MemberAccessibleValue) + + fieldValue := vm.pop() + + // VM assumes the field name is always a string. + fieldName := getStringConstant(vm, ins.FieldNameIndex) + + structValue.SetMember(vm.config, fieldName, fieldValue) +} + +func opGetField(vm *VM, ins opcode.InstructionGetField) { + memberAccessibleValue := vm.pop().(MemberAccessibleValue) + + // VM assumes the field name is always a string. + fieldName := getStringConstant(vm, ins.FieldNameIndex) + + fieldValue := memberAccessibleValue.GetMember(vm.config, fieldName) + if fieldValue == nil { + panic(MissingMemberValueError{ + Parent: memberAccessibleValue, + Name: fieldName, + }) + } + + vm.push(fieldValue) +} + +func getStringConstant(vm *VM, index uint16) string { + constant := vm.callFrame.executable.Program.Constants[index] + return string(constant.Data) +} + +func opTransfer(vm *VM, ins opcode.InstructionTransfer) { + targetType := vm.loadType(ins.TypeIndex) + value := vm.peek() + + config := vm.config + + transferredValue := value.Transfer( + config, + atree.Address{}, + false, nil, + ) + + valueType := transferredValue.StaticType(config) + if !IsSubType(config, valueType, targetType) { + panic(errors.NewUnexpectedError("invalid transfer: expected '%s', found '%s'", targetType, valueType)) + } + + vm.replaceTop(transferredValue) +} + +func opDestroy(vm *VM) { + value := vm.pop().(*CompositeValue) + value.Destroy(vm.config) +} + +func opPath(vm *VM, ins opcode.InstructionPath) { + identifier := getStringConstant(vm, ins.IdentifierIndex) + value := PathValue{ + Domain: ins.Domain, + Identifier: identifier, + } + vm.push(value) +} + +func opCast(vm *VM, ins opcode.InstructionCast) { + value := vm.pop() + + targetType := vm.loadType(ins.TypeIndex) + + // TODO: + _ = ins.Kind + _ = targetType + + vm.push(value) +} + +func opNil(vm *VM) { + vm.push(NilValue{}) +} + +func opEqual(vm *VM) { + left, right := vm.peekPop() + vm.replaceTop(BoolValue(left == right)) +} + +func opNotEqual(vm *VM) { + left, right := vm.peekPop() + vm.replaceTop(BoolValue(left != right)) +} + +func opUnwrap(vm *VM) { + value := vm.peek() + if someValue, ok := value.(*SomeValue); ok { + value = someValue.value + vm.replaceTop(value) + } +} + +func opNewArray(vm *VM, ins opcode.InstructionNewArray) { + + typ := vm.loadType(ins.TypeIndex).(interpreter.ArrayStaticType) + + elements := make([]Value, ins.Size) + + // Must be inserted in the reverse, + // since the stack if FILO. + for i := int(ins.Size) - 1; i >= 0; i-- { + elements[i] = vm.pop() + } + + array := NewArrayValue(vm.config, typ, ins.IsResource, elements...) + vm.push(array) +} + +func opNewRef(vm *VM, ins opcode.InstructionNewRef) { + + borrowedType := vm.loadType(ins.TypeIndex).(*interpreter.ReferenceStaticType) + value := vm.pop() + + ref := NewEphemeralReferenceValue( + vm.config, + value, + borrowedType.Authorization, + borrowedType.ReferencedType, + ) + vm.push(ref) +} + +func (vm *VM) run() { + for { + + callFrame := vm.callFrame + + if len(vm.callstack) == 0 || + int(vm.ip) >= len(callFrame.function.Code) { + + return + } + + ins := callFrame.function.Code[vm.ip] + vm.ip++ + + switch ins := ins.(type) { + case opcode.InstructionReturnValue: + opReturnValue(vm) + case opcode.InstructionReturn: + opReturn(vm) + case opcode.InstructionJump: + opJump(vm, ins) + case opcode.InstructionJumpIfFalse: + opJumpIfFalse(vm, ins) + case opcode.InstructionIntAdd: + opBinaryIntAdd(vm) + case opcode.InstructionIntSubtract: + opBinaryIntSubtract(vm) + case opcode.InstructionIntLess: + opBinaryIntLess(vm) + case opcode.InstructionIntGreater: + opBinaryIntGreater(vm) + case opcode.InstructionTrue: + opTrue(vm) + case opcode.InstructionFalse: + opFalse(vm) + case opcode.InstructionGetConstant: + opGetConstant(vm, ins) + case opcode.InstructionGetLocal: + opGetLocal(vm, ins) + case opcode.InstructionSetLocal: + opSetLocal(vm, ins) + case opcode.InstructionGetGlobal: + opGetGlobal(vm, ins) + case opcode.InstructionSetGlobal: + opSetGlobal(vm, ins) + case opcode.InstructionSetIndex: + opSetIndex(vm) + case opcode.InstructionGetIndex: + opGetIndex(vm) + case opcode.InstructionInvoke: + opInvoke(vm, ins) + case opcode.InstructionInvokeDynamic: + opInvokeDynamic(vm, ins) + case opcode.InstructionDrop: + opDrop(vm) + case opcode.InstructionDup: + opDup(vm) + case opcode.InstructionNew: + opNew(vm, ins) + case opcode.InstructionNewArray: + opNewArray(vm, ins) + case opcode.InstructionNewRef: + opNewRef(vm, ins) + case opcode.InstructionSetField: + opSetField(vm, ins) + case opcode.InstructionGetField: + opGetField(vm, ins) + case opcode.InstructionTransfer: + opTransfer(vm, ins) + case opcode.InstructionDestroy: + opDestroy(vm) + case opcode.InstructionPath: + opPath(vm, ins) + case opcode.InstructionCast: + opCast(vm, ins) + case opcode.InstructionNil: + opNil(vm) + case opcode.InstructionEqual: + opEqual(vm) + case opcode.InstructionNotEqual: + opNotEqual(vm) + case opcode.InstructionUnwrap: + opUnwrap(vm) + default: + panic(errors.NewUnexpectedError("cannot execute instruction of type %T", ins)) + } + + // Faster in Go <1.19: + // vmOps[op](vm) + } +} + +func (vm *VM) initializeConstant(index uint16) (value Value) { + executable := vm.callFrame.executable + + constant := executable.Program.Constants[index] + switch constant.Kind { + case constantkind.Int: + // TODO: + smallInt, _, _ := leb128.ReadInt64(constant.Data) + value = NewIntValue(smallInt) + case constantkind.String: + value = NewStringValueFromBytes(constant.Data) + default: + // TODO: + panic(errors.NewUnexpectedError("unsupported constant kind '%s'", constant.Kind.String())) + } + + executable.Constants[index] = value + return value +} + +func (vm *VM) loadType(index uint16) StaticType { + staticType := vm.callFrame.executable.StaticTypes[index] + if staticType == nil { + // TODO: Remove. Should never reach because of the + // pre loading-decoding of types. + staticType = vm.initializeType(index) + } + + return staticType +} + +func (vm *VM) initializeType(index uint16) interpreter.StaticType { + executable := vm.callFrame.executable + typeBytes := executable.Program.Types[index] + staticType := decodeType(typeBytes) + executable.StaticTypes[index] = staticType + return staticType +} + +func decodeType(typeBytes []byte) interpreter.StaticType { + dec := interpreter.CBORDecMode.NewByteStreamDecoder(typeBytes) + staticType, err := interpreter.NewTypeDecoder(dec, nil).DecodeStaticType() + if err != nil { + panic(err) + } + return staticType +} + +func (vm *VM) lookupFunction(location common.Location, name string) FunctionValue { + // First check in current program. + value, ok := vm.globals[name] + if ok { + return value.(FunctionValue) + } + + // If not found, check in already linked imported functions. + linkedGlobals, ok := vm.linkedGlobalsCache[location] + + // If not found, link the function now, dynamically. + if !ok { + // TODO: This currently link all functions in program, unnecessarily. + // Link only the requested function. + program := vm.config.ImportHandler(location) + + linkedGlobals = LinkGlobals( + location, + program, + vm.config, + vm.linkedGlobalsCache, + ) + } + + value, ok = linkedGlobals.indexedGlobals[name] + if !ok { + panic(errors.NewUnexpectedError("cannot link global: %s", name)) + } + + return value.(FunctionValue) +} + +func (vm *VM) StackSize() int { + return len(vm.stack) +} + +func getReceiver[T any](config *Config, receiver Value) T { + switch receiver := receiver.(type) { + case *SomeValue: + return getReceiver[T](config, receiver.value) + case *EphemeralReferenceValue: + return getReceiver[T](config, receiver.Value) + case *StorageReferenceValue: + referencedValue, err := receiver.dereference(nil) + if err != nil { + panic(err) + } + return getReceiver[T](config, *referencedValue) + default: + return receiver.(T) + } +} diff --git a/go.mod b/go.mod index 6193bce06e..7db9c01457 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,7 @@ require ( require ( github.com/SaveTheRbtz/mph v0.1.1-0.20240117162131-4166ec7869bc + github.com/goccy/go-yaml v1.15.9 github.com/k0kubun/pp v3.0.1+incompatible github.com/kodova/html-to-markdown v1.0.1 github.com/onflow/crypto v0.25.0 diff --git a/go.sum b/go.sum index cc2c3058d0..b7f332a032 100644 --- a/go.sum +++ b/go.sum @@ -22,6 +22,8 @@ github.com/fxamacker/cbor/v2 v2.4.1-0.20230228173756-c0c9f774e40c h1:5tm/Wbs9d9r github.com/fxamacker/cbor/v2 v2.4.1-0.20230228173756-c0c9f774e40c/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/fxamacker/circlehash v0.3.0 h1:XKdvTtIJV9t7DDUtsf0RIpC1OcxZtPbmgIH7ekx28WA= github.com/fxamacker/circlehash v0.3.0/go.mod h1:3aq3OfVvsWtkWMb6A1owjOQFA+TLsD5FgJflnaQwtMM= +github.com/goccy/go-yaml v1.15.9 h1:500CYajdgpK4Smqrf86u7VMZuj/bFt2ghdff9D/nQYc= +github.com/goccy/go-yaml v1.15.9/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= diff --git a/interpreter/encode.go b/interpreter/encode.go index 02a86e5819..1af6642f79 100644 --- a/interpreter/encode.go +++ b/interpreter/encode.go @@ -916,7 +916,7 @@ const ( encodedAddressLocationLength = 2 ) -func encodeLocation(e *cbor.StreamEncoder, l common.Location) error { +func EncodeLocation(e *cbor.StreamEncoder, l common.Location) error { if l == nil { return e.EncodeNil() } @@ -1283,7 +1283,7 @@ func (t *CompositeStaticType) Encode(e *cbor.StreamEncoder) error { } // Encode location at array index encodedCompositeStaticTypeLocationFieldKey - err = encodeLocation(e, t.Location) + err = EncodeLocation(e, t.Location) if err != nil { return err } @@ -1326,7 +1326,7 @@ func (t *InterfaceStaticType) Encode(e *cbor.StreamEncoder) error { } // Encode location at array index encodedInterfaceStaticTypeLocationFieldKey - err = encodeLocation(e, t.Location) + err = EncodeLocation(e, t.Location) if err != nil { return err } @@ -1654,11 +1654,11 @@ func (t FunctionStaticType) Encode(_ *cbor.StreamEncoder) error { } } -// compositeTypeInfo -type compositeTypeInfo struct { - location common.Location - qualifiedIdentifier string - kind common.CompositeKind +// CompositeTypeInfo +type CompositeTypeInfo struct { + Location common.Location + QualifiedIdentifier string + Kind common.CompositeKind } func NewCompositeTypeInfo( @@ -1666,34 +1666,34 @@ func NewCompositeTypeInfo( location common.Location, qualifiedIdentifier string, kind common.CompositeKind, -) compositeTypeInfo { +) CompositeTypeInfo { common.UseMemory(memoryGauge, common.CompositeTypeInfoMemoryUsage) - return compositeTypeInfo{ - location: location, - qualifiedIdentifier: qualifiedIdentifier, - kind: kind, + return CompositeTypeInfo{ + Location: location, + QualifiedIdentifier: qualifiedIdentifier, + Kind: kind, } } -var _ atree.TypeInfo = compositeTypeInfo{} +var _ atree.TypeInfo = CompositeTypeInfo{} const encodedCompositeTypeInfoLength = 3 -func (c compositeTypeInfo) IsComposite() bool { +func (c CompositeTypeInfo) IsComposite() bool { return true } -func (c compositeTypeInfo) Identifier() string { - return string(c.location.TypeID(nil, c.qualifiedIdentifier)) +func (c CompositeTypeInfo) Identifier() string { + return string(c.Location.TypeID(nil, c.QualifiedIdentifier)) } -func (c compositeTypeInfo) Copy() atree.TypeInfo { +func (c CompositeTypeInfo) Copy() atree.TypeInfo { // Return c as is because c is a value type. return c } -func (c compositeTypeInfo) Encode(e *cbor.StreamEncoder) error { +func (c CompositeTypeInfo) Encode(e *cbor.StreamEncoder) error { err := e.EncodeRawBytes([]byte{ // tag number 0xd8, CBORTagCompositeValue, @@ -1704,17 +1704,17 @@ func (c compositeTypeInfo) Encode(e *cbor.StreamEncoder) error { return err } - err = encodeLocation(e, c.location) + err = EncodeLocation(e, c.Location) if err != nil { return err } - err = e.EncodeString(c.qualifiedIdentifier) + err = e.EncodeString(c.QualifiedIdentifier) if err != nil { return err } - err = e.EncodeUint64(uint64(c.kind)) + err = e.EncodeUint64(uint64(c.Kind)) if err != nil { return err } @@ -1722,12 +1722,12 @@ func (c compositeTypeInfo) Encode(e *cbor.StreamEncoder) error { return nil } -func (c compositeTypeInfo) Equal(o atree.TypeInfo) bool { - other, ok := o.(compositeTypeInfo) +func (c CompositeTypeInfo) Equal(o atree.TypeInfo) bool { + other, ok := o.(CompositeTypeInfo) return ok && - c.location == other.location && - c.qualifiedIdentifier == other.qualifiedIdentifier && - c.kind == other.kind + c.Location == other.Location && + c.QualifiedIdentifier == other.QualifiedIdentifier && + c.Kind == other.Kind } // EmptyTypeInfo diff --git a/interpreter/interpreter.go b/interpreter/interpreter.go index a163efb01f..80def48c42 100644 --- a/interpreter/interpreter.go +++ b/interpreter/interpreter.go @@ -5232,7 +5232,7 @@ func (interpreter *Interpreter) ValidateAtreeValue(value atree.Value) { return info.Equal(other.(StaticType)) case *DictionaryStaticType: return info.Equal(other.(StaticType)) - case compositeTypeInfo: + case CompositeTypeInfo: return info.Equal(other) case EmptyTypeInfo: _, ok := other.(EmptyTypeInfo) diff --git a/interpreter/storage.go b/interpreter/storage.go index c5bdcb0b87..f9c6eb6d59 100644 --- a/interpreter/storage.go +++ b/interpreter/storage.go @@ -75,15 +75,15 @@ func ConvertStoredValue(gauge common.MemoryGauge, value atree.Value) (Value, err typeInfo := value.Type() switch staticType := typeInfo.(type) { case *DictionaryStaticType: - return newDictionaryValueFromAtreeMap( + return NewDictionaryValueFromAtreeMap( gauge, staticType, DictionaryElementSize(staticType), value, ), nil - case compositeTypeInfo: - return newCompositeValueFromAtreeMap( + case CompositeTypeInfo: + return NewCompositeValueFromAtreeMap( gauge, staticType, value, diff --git a/interpreter/value_composite.go b/interpreter/value_composite.go index d11311d543..d19d4aef59 100644 --- a/interpreter/value_composite.go +++ b/interpreter/value_composite.go @@ -197,7 +197,7 @@ func NewCompositeValue( func newCompositeValueFromConstructor( gauge common.MemoryGauge, count uint64, - typeInfo compositeTypeInfo, + typeInfo CompositeTypeInfo, constructor func() *atree.OrderedMap, ) *CompositeValue { @@ -207,16 +207,16 @@ func newCompositeValueFromConstructor( common.UseMemory(gauge, dataUse) common.UseMemory(gauge, metaDataUse) - return newCompositeValueFromAtreeMap( + return NewCompositeValueFromAtreeMap( gauge, typeInfo, constructor(), ) } -func newCompositeValueFromAtreeMap( +func NewCompositeValueFromAtreeMap( gauge common.MemoryGauge, - typeInfo compositeTypeInfo, + typeInfo CompositeTypeInfo, atreeOrderedMap *atree.OrderedMap, ) *CompositeValue { @@ -224,9 +224,9 @@ func newCompositeValueFromAtreeMap( return &CompositeValue{ dictionary: atreeOrderedMap, - Location: typeInfo.location, - QualifiedIdentifier: typeInfo.qualifiedIdentifier, - Kind: typeInfo.kind, + Location: typeInfo.Location, + QualifiedIdentifier: typeInfo.QualifiedIdentifier, + Kind: typeInfo.Kind, } } @@ -1317,7 +1317,7 @@ func (v *CompositeValue) Transfer( v.Kind, ) - res := newCompositeValueFromAtreeMap( + res := NewCompositeValueFromAtreeMap( interpreter, info, dictionary, @@ -1938,3 +1938,7 @@ func (v *CompositeValue) ForEach( } } } + +func (v *CompositeValue) AtreeMap() *atree.OrderedMap { + return v.dictionary +} diff --git a/interpreter/value_dictionary.go b/interpreter/value_dictionary.go index b7b804c465..a8d1f42197 100644 --- a/interpreter/value_dictionary.go +++ b/interpreter/value_dictionary.go @@ -217,7 +217,7 @@ func newDictionaryValueFromConstructor( common.UseMemory(gauge, dataSlabs) common.UseMemory(gauge, metaDataSlabs) - return newDictionaryValueFromAtreeMap( + return NewDictionaryValueFromAtreeMap( gauge, staticType, elementSize, @@ -225,7 +225,7 @@ func newDictionaryValueFromConstructor( ) } -func newDictionaryValueFromAtreeMap( +func NewDictionaryValueFromAtreeMap( gauge common.MemoryGauge, staticType *DictionaryStaticType, elementSize uint, @@ -1430,7 +1430,7 @@ func (v *DictionaryValue) Transfer( v.dictionary = nil } - res := newDictionaryValueFromAtreeMap( + res := NewDictionaryValueFromAtreeMap( interpreter, v.Type, v.elementSize, @@ -1486,7 +1486,7 @@ func (v *DictionaryValue) Clone(interpreter *Interpreter) Value { panic(errors.NewExternalError(err)) } - dictionary := newDictionaryValueFromAtreeMap( + dictionary := NewDictionaryValueFromAtreeMap( interpreter, v.Type, v.elementSize, @@ -1586,3 +1586,11 @@ func (v *DictionaryValue) SetType(staticType *DictionaryStaticType) { panic(errors.NewExternalError(err)) } } + +func (v *DictionaryValue) AtreeMap() *atree.OrderedMap { + return v.dictionary +} + +func (v *DictionaryValue) ElementSize() uint { + return v.elementSize +} diff --git a/runtime/runtime.go b/runtime/runtime.go index e385f2c0c7..44fdd08211 100644 --- a/runtime/runtime.go +++ b/runtime/runtime.go @@ -227,11 +227,11 @@ func (r *interpreterRuntime) Recover(onError func(Error), location Location, cod return } - err := getWrappedError(recovered, location, codesAndPrograms) + err := GetWrappedError(recovered, location, codesAndPrograms) onError(err) } -func getWrappedError(recovered any, location Location, codesAndPrograms CodesAndPrograms) Error { +func GetWrappedError(recovered any, location Location, codesAndPrograms CodesAndPrograms) Error { switch recovered := recovered.(type) { // If the error is already a `runtime.Error`, then avoid redundant wrapping. diff --git a/runtime/runtime_test.go b/runtime/runtime_test.go index 7f942cbddf..3c41142877 100644 --- a/runtime/runtime_test.go +++ b/runtime/runtime_test.go @@ -11734,3 +11734,110 @@ func TestRuntimeBuiltInFunctionConfusion(t *testing.T) { var redeclarationError *sema.RedeclarationError require.ErrorAs(t, err, &redeclarationError) } + +func BenchmarkContractFunctionInvocation(b *testing.B) { + + runtime := NewTestInterpreterRuntime() + + addressValue := cadence.BytesToAddress([]byte{0x1}) + + contract := []byte(` + pub contract Test { + pub fun helloText(): String { + return "global function of the imported program" + } + + init() {} + + pub struct Foo { + pub var id : String + + init(_ id: String) { + self.id = id + } + + pub fun sayHello(_ id: Int): String { + // return self.id + return Test.helloText() + } + } + } + `) + + deploy := DeploymentTransaction("Test", contract) + + var accountCode []byte + var events []cadence.Event + + runtimeInterface := &TestRuntimeInterface{ + OnGetCode: func(_ Location) (bytes []byte, err error) { + return accountCode, nil + }, + Storage: NewTestLedger(nil, nil), + OnGetSigningAccounts: func() ([]Address, error) { + return []Address{Address(addressValue)}, nil + }, + OnResolveLocation: NewSingleIdentifierLocationResolver(b), + OnGetAccountContractCode: func(_ common.AddressLocation) (code []byte, err error) { + return accountCode, nil + }, + OnUpdateAccountContractCode: func(_ common.AddressLocation, code []byte) error { + accountCode = code + return nil + }, + OnEmitEvent: func(event cadence.Event) error { + events = append(events, event) + return nil + }, + OnDecodeArgument: func(b []byte, t cadence.Type) (cadence.Value, error) { + return json.Decode(nil, b) + }, + } + + nextTransactionLocation := NewTransactionLocationGenerator() + + err := runtime.ExecuteTransaction( + Script{ + Source: deploy, + }, + Context{ + Interface: runtimeInterface, + Location: nextTransactionLocation(), + }, + ) + require.NoError(b, err) + assert.NotNil(b, accountCode) + + script := ` + import Test from 0x01 + + pub fun main(count: Int): String { + var i = 0 + var r = Test.Foo("Hello from Foo!") + while i < count { + i = i + 1 + r = Test.Foo("Hello from Foo!") + r.sayHello(1) + } + return r.sayHello(1) + }` + + args := encodeArgs([]cadence.Value{cadence.NewInt(7)}) + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + _, err := runtime.ExecuteScript( + Script{ + Source: []byte(script), + Arguments: args, + }, + Context{ + Interface: runtimeInterface, + Location: nextTransactionLocation(), + }, + ) + require.NoError(b, err) + } +} diff --git a/stdlib/account.go b/stdlib/account.go index 97a8e4d0f3..e9f323cc78 100644 --- a/stdlib/account.go +++ b/stdlib/account.go @@ -2204,7 +2204,7 @@ const getAccountFunctionDocString = ` Returns the account for the given address ` -var getAccountFunctionType = sema.NewSimpleFunctionType( +var GetAccountFunctionType = sema.NewSimpleFunctionType( sema.FunctionPurityView, []sema.Parameter{ { @@ -2219,7 +2219,7 @@ var getAccountFunctionType = sema.NewSimpleFunctionType( func NewGetAccountFunction(handler AccountHandler) StandardLibraryValue { return NewStandardLibraryStaticFunction( "getAccount", - getAccountFunctionType, + GetAccountFunctionType, getAccountFunctionDocString, func(invocation interpreter.Invocation) interpreter.Value { @@ -3025,7 +3025,7 @@ func removeCapabilityController( panic(errors.NewUnreachableError()) } - setCapabilityControllerTag( + SetCapabilityControllerTag( inter, address, uint64(capabilityIDValue), @@ -4487,7 +4487,7 @@ func newCapabilityControllerGetTagFunction( } } -func setCapabilityControllerTag( +func SetCapabilityControllerTag( inter *interpreter.Interpreter, address common.Address, capabilityID uint64, @@ -4512,7 +4512,7 @@ func newCapabilityControllerSetTagFunction( capabilityIDValue interpreter.UInt64Value, ) func(*interpreter.Interpreter, *interpreter.StringValue) { return func(inter *interpreter.Interpreter, tagValue *interpreter.StringValue) { - setCapabilityControllerTag( + SetCapabilityControllerTag( inter, address, uint64(capabilityIDValue), diff --git a/stdlib/panic.go b/stdlib/panic.go index 054fe5cdd0..baf3e5b68b 100644 --- a/stdlib/panic.go +++ b/stdlib/panic.go @@ -43,7 +43,7 @@ const panicFunctionDocString = ` Terminates the program unconditionally and reports a message which explains why the unrecoverable error occurred. ` -var panicFunctionType = sema.NewSimpleFunctionType( +var PanicFunctionType = sema.NewSimpleFunctionType( sema.FunctionPurityView, []sema.Parameter{ { @@ -57,7 +57,7 @@ var panicFunctionType = sema.NewSimpleFunctionType( var PanicFunction = NewStandardLibraryStaticFunction( "panic", - panicFunctionType, + PanicFunctionType, panicFunctionDocString, func(invocation interpreter.Invocation) interpreter.Value { messageValue, ok := invocation.Arguments[0].(*interpreter.StringValue) diff --git a/tools/storage-explorer/addresses.go b/tools/storage-explorer/addresses.go index 8a291d7a75..6fb06cdc47 100644 --- a/tools/storage-explorer/addresses.go +++ b/tools/storage-explorer/addresses.go @@ -24,7 +24,7 @@ import ( "github.com/onflow/flow-go/cmd/util/ledger/util/registers" - "github.com/onflow/cadence/runtime/common" + "github.com/onflow/cadence/common" ) func addressesJSON(registersByAccount *registers.ByAccount) ([]byte, error) { diff --git a/tools/storage-explorer/main.go b/tools/storage-explorer/main.go index fe4f80504f..d9123f955c 100644 --- a/tools/storage-explorer/main.go +++ b/tools/storage-explorer/main.go @@ -35,11 +35,11 @@ import ( "github.com/onflow/flow-go/model/flow" "github.com/rs/zerolog" - "github.com/onflow/cadence/runtime/interpreter" + "github.com/onflow/cadence/interpreter" + "github.com/onflow/cadence/common" jsoncdc "github.com/onflow/cadence/encoding/json" "github.com/onflow/cadence/runtime" - "github.com/onflow/cadence/runtime/common" ) func main() { diff --git a/tools/storage-explorer/storagemaps.go b/tools/storage-explorer/storagemaps.go index ba0ca4ed91..e173291865 100644 --- a/tools/storage-explorer/storagemaps.go +++ b/tools/storage-explorer/storagemaps.go @@ -24,10 +24,10 @@ import ( "github.com/onflow/atree" + "github.com/onflow/cadence/common" + "github.com/onflow/cadence/interpreter" "github.com/onflow/cadence/runtime" - "github.com/onflow/cadence/runtime/common" - "github.com/onflow/cadence/runtime/interpreter" - "github.com/onflow/cadence/runtime/stdlib" + "github.com/onflow/cadence/stdlib" ) type KnownStorageMap struct { diff --git a/tools/storage-explorer/type.go b/tools/storage-explorer/type.go index 24a7a93416..7d1b413078 100644 --- a/tools/storage-explorer/type.go +++ b/tools/storage-explorer/type.go @@ -21,9 +21,9 @@ package main import ( "github.com/onflow/cadence" jsoncdc "github.com/onflow/cadence/encoding/json" + "github.com/onflow/cadence/interpreter" "github.com/onflow/cadence/runtime" - "github.com/onflow/cadence/runtime/interpreter" - "github.com/onflow/cadence/runtime/sema" + "github.com/onflow/cadence/sema" ) func prepareType(value interpreter.Value, inter *interpreter.Interpreter) (result any, description string) { diff --git a/tools/storage-explorer/value.go b/tools/storage-explorer/value.go index 9355456417..a7545ae5d3 100644 --- a/tools/storage-explorer/value.go +++ b/tools/storage-explorer/value.go @@ -23,9 +23,9 @@ import ( "sort" jsoncdc "github.com/onflow/cadence/encoding/json" + "github.com/onflow/cadence/interpreter" "github.com/onflow/cadence/runtime" - "github.com/onflow/cadence/runtime/interpreter" - "github.com/onflow/cadence/runtime/sema" + "github.com/onflow/cadence/sema" ) type Value interface {