diff --git a/runtime/bbq/compiler/compiler.go b/runtime/bbq/compiler/compiler.go new file mode 100644 index 0000000000..2ede219c78 --- /dev/null +++ b/runtime/bbq/compiler/compiler.go @@ -0,0 +1,547 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright 2019-2022 Dapper Labs, Inc. + * + * 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/runtime/ast" + "github.com/onflow/cadence/runtime/bbq" + "github.com/onflow/cadence/runtime/bbq/constantkind" + "github.com/onflow/cadence/runtime/bbq/leb128" + "github.com/onflow/cadence/runtime/bbq/opcode" + "github.com/onflow/cadence/runtime/errors" + "github.com/onflow/cadence/runtime/sema" +) + +type Compiler struct { + Program *ast.Program + Elaboration *sema.Elaboration + + currentFunction *function + functions []*function + constants []*constant + globals map[string]*global + loops []*loop + currentLoop *loop +} + +var _ ast.DeclarationVisitor[struct{}] = &Compiler{} +var _ ast.StatementVisitor[struct{}] = &Compiler{} +var _ ast.ExpressionVisitor[struct{}] = &Compiler{} + +func NewCompiler( + program *ast.Program, + elaboration *sema.Elaboration, +) *Compiler { + return &Compiler{ + Program: program, + Elaboration: elaboration, + globals: map[string]*global{}, + } +} + +func (c *Compiler) findGlobal(name string) *global { + return c.globals[name] +} + +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) addFunction(name string, parameterCount uint16) *function { + c.addGlobal(name) + function := newFunction(name, parameterCount) + c.functions = append(c.functions, function) + c.currentFunction = function + return function +} + +func (c *Compiler) addConstant(kind constantkind.Constant, data []byte) *constant { + count := len(c.constants) + if count >= math.MaxUint16 { + panic(errors.NewDefaultUserError("invalid constant declaration")) + } + constant := &constant{ + index: uint16(count), + kind: kind, + data: data[:], + } + c.constants = append(c.constants, constant) + return constant +} + +func (c *Compiler) emit(opcode opcode.Opcode, args ...byte) int { + return c.currentFunction.emit(opcode, args...) +} + +func (c *Compiler) emitUndefinedJump(opcode opcode.Opcode) int { + return c.emit(opcode, 0xff, 0xff) +} + +func (c *Compiler) emitJump(opcode opcode.Opcode, target int) int { + if target >= math.MaxUint16 { + panic(errors.NewDefaultUserError("invalid jump")) + } + first, second := encodeUint16(uint16(target)) + return c.emit(opcode, first, second) +} + +func (c *Compiler) patchJump(opcodeOffset int) { + code := c.currentFunction.code + count := len(code) + if count == 0 { + panic(errors.NewUnreachableError()) + } + if count >= math.MaxUint16 { + panic(errors.NewDefaultUserError("invalid jump")) + } + target := uint16(count) + first, second := encodeUint16(target) + code[opcodeOffset+1] = first + code[opcodeOffset+2] = second +} + +// encodeUint16 encodes the given uint16 in big-endian representation +func encodeUint16(jump uint16) (byte, byte) { + return byte((jump >> 8) & 0xff), + byte(jump & 0xff) +} + +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) Compile() *bbq.Program { + for _, declaration := range c.Program.Declarations() { + c.compileDeclaration(declaration) + } + + functions := c.exportFunctions() + constants := c.exportConstants() + + return &bbq.Program{ + Functions: functions, + Constants: constants, + } +} + +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) exportFunctions() []*bbq.Function { + functions := make([]*bbq.Function, 0, len(c.functions)) + for _, function := range c.functions { + functions = append( + functions, + &bbq.Function{ + Name: function.name, + Code: function.code, + LocalCount: function.localCount, + ParameterCount: function.parameterCount, + }, + ) + } + return functions +} + +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 + 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.emit(opcode.ReturnValue) + } else { + c.emit(opcode.Return) + } + return +} + +func (c *Compiler) VisitBreakStatement(_ *ast.BreakStatement) (_ struct{}) { + offset := len(c.currentFunction.code) + c.currentLoop.breaks = append(c.currentLoop.breaks, offset) + c.emitUndefinedJump(opcode.Jump) + return +} + +func (c *Compiler) VisitContinueStatement(_ *ast.ContinueStatement) (_ struct{}) { + c.emitJump(opcode.Jump, 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.emitUndefinedJump(opcode.JumpIfFalse) + c.compileBlock(statement.Then) + elseBlock := statement.Else + if elseBlock != nil { + thenJump := c.emit(opcode.Jump) + c.patchJump(elseJump) + c.compileBlock(elseBlock) + c.patchJump(thenJump) + } else { + c.patchJump(elseJump) + } + return +} + +func (c *Compiler) VisitWhileStatement(statement *ast.WhileStatement) (_ struct{}) { + testOffset := len(c.currentFunction.code) + c.pushLoop(testOffset) + c.compileExpression(statement.Test) + endJump := c.emitUndefinedJump(opcode.JumpIfFalse) + c.compileBlock(statement.Block) + c.emitJump(opcode.Jump, 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) + local := c.currentFunction.declareLocal(declaration.Identifier.Identifier) + first, second := encodeUint16(local.index) + c.emit(opcode.SetLocal, first, second) + return +} + +func (c *Compiler) VisitAssignmentStatement(statement *ast.AssignmentStatement) (_ struct{}) { + c.compileExpression(statement.Value) + switch target := statement.Target.(type) { + case *ast.IdentifierExpression: + local := c.currentFunction.findLocal(target.Identifier.Identifier) + first, second := encodeUint16(local.index) + c.emit(opcode.SetLocal, first, second) + default: + // TODO: + panic(errors.NewUnreachableError()) + } + return +} + +func (c *Compiler) VisitSwapStatement(_ *ast.SwapStatement) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +func (c *Compiler) VisitExpressionStatement(_ *ast.ExpressionStatement) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +func (c *Compiler) VisitVoidExpression(_ *ast.VoidExpression) (_ struct{}) { + //TODO + panic(errors.NewUnreachableError()) +} + +func (c *Compiler) VisitBoolExpression(expression *ast.BoolExpression) (_ struct{}) { + if expression.Value { + c.emit(opcode.True) + } else { + c.emit(opcode.False) + } + return +} + +func (c *Compiler) VisitNilExpression(_ *ast.NilExpression) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +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) + first, second := encodeUint16(constant.index) + c.emit(opcode.GetConstant, first, second) + return +} + +func (c *Compiler) VisitFixedPointExpression(_ *ast.FixedPointExpression) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +func (c *Compiler) VisitArrayExpression(_ *ast.ArrayExpression) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +func (c *Compiler) VisitDictionaryExpression(_ *ast.DictionaryExpression) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +func (c *Compiler) VisitIdentifierExpression(expression *ast.IdentifierExpression) (_ struct{}) { + name := expression.Identifier.Identifier + local := c.currentFunction.findLocal(name) + if local != nil { + first, second := encodeUint16(local.index) + c.emit(opcode.GetLocal, first, second) + return + } + global := c.findGlobal(name) + first, second := encodeUint16(global.index) + c.emit(opcode.GetGlobal, first, second) + return +} + +func (c *Compiler) VisitInvocationExpression(expression *ast.InvocationExpression) (_ struct{}) { + // TODO: copy + for _, argument := range expression.Arguments { + c.compileExpression(argument.Expression) + } + c.compileExpression(expression.InvokedExpression) + c.emit(opcode.Call) + return +} + +func (c *Compiler) VisitMemberExpression(_ *ast.MemberExpression) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +func (c *Compiler) VisitIndexExpression(_ *ast.IndexExpression) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +func (c *Compiler) VisitConditionalExpression(_ *ast.ConditionalExpression) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +func (c *Compiler) VisitUnaryExpression(_ *ast.UnaryExpression) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +func (c *Compiler) VisitBinaryExpression(expression *ast.BinaryExpression) (_ struct{}) { + c.compileExpression(expression.Left) + c.compileExpression(expression.Right) + // TODO: add support for other types + c.emit(intBinaryOpcodes[expression.Operation]) + return +} + +var intBinaryOpcodes = [...]opcode.Opcode{ + ast.OperationPlus: opcode.IntAdd, + ast.OperationMinus: opcode.IntSubtract, + ast.OperationMul: opcode.IntMultiply, + ast.OperationDiv: opcode.IntDivide, + ast.OperationMod: opcode.IntMod, + ast.OperationEqual: opcode.IntEqual, + ast.OperationNotEqual: opcode.IntNotEqual, + ast.OperationLess: opcode.IntLess, + ast.OperationLessEqual: opcode.IntLessOrEqual, + ast.OperationGreater: opcode.IntGreater, + ast.OperationGreaterEqual: opcode.IntGreaterOrEqual, +} + +func (c *Compiler) VisitFunctionExpression(_ *ast.FunctionExpression) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +func (c *Compiler) VisitStringExpression(_ *ast.StringExpression) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +func (c *Compiler) VisitCastingExpression(_ *ast.CastingExpression) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +func (c *Compiler) VisitCreateExpression(_ *ast.CreateExpression) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +func (c *Compiler) VisitDestroyExpression(_ *ast.DestroyExpression) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +func (c *Compiler) VisitReferenceExpression(_ *ast.ReferenceExpression) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +func (c *Compiler) VisitForceExpression(_ *ast.ForceExpression) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +func (c *Compiler) VisitPathExpression(_ *ast.PathExpression) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +func (c *Compiler) VisitSpecialFunctionDeclaration(declaration *ast.SpecialFunctionDeclaration) (_ struct{}) { + return c.VisitFunctionDeclaration(declaration.FunctionDeclaration) +} + +func (c *Compiler) VisitFunctionDeclaration(declaration *ast.FunctionDeclaration) (_ struct{}) { + // TODO: handle nested functions + functionName := declaration.Identifier.Identifier + functionType := c.Elaboration.FunctionDeclarationFunctionTypes[declaration] + parameterCount := len(functionType.Parameters) + if parameterCount > math.MaxUint16 { + panic(errors.NewDefaultUserError("invalid parameter count")) + } + function := c.addFunction(functionName, uint16(parameterCount)) + for _, parameter := range declaration.ParameterList.Parameters { + parameterName := parameter.Identifier.Identifier + function.declareLocal(parameterName) + } + c.compileFunctionBlock(declaration.FunctionBlock) + return +} + +func (c *Compiler) VisitCompositeDeclaration(_ *ast.CompositeDeclaration) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +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(_ *ast.ImportDeclaration) (_ struct{}) { + // TODO + panic(errors.NewUnreachableError()) +} + +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) + } +} diff --git a/runtime/bbq/compiler/compiler_test.go b/runtime/bbq/compiler/compiler_test.go new file mode 100644 index 0000000000..92db712d92 --- /dev/null +++ b/runtime/bbq/compiler/compiler_test.go @@ -0,0 +1,331 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright 2019-2022 Dapper Labs, Inc. + * + * 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/runtime/bbq" + "github.com/onflow/cadence/runtime/bbq/constantkind" + "github.com/onflow/cadence/runtime/bbq/opcode" + . "github.com/onflow/cadence/runtime/tests/checker" +) + +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 := NewCompiler(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.GetGlobal), 0, 0, + byte(opcode.Call), + // fib(n - 2) + byte(opcode.GetLocal), 0, 0, + byte(opcode.GetConstant), 0, 2, + byte(opcode.IntSubtract), + byte(opcode.GetGlobal), 0, 0, + byte(opcode.Call), + // return sum + byte(opcode.IntAdd), + byte(opcode.ReturnValue), + }, + compiler.functions[0].code, + ) + + require.Equal(t, + []*bbq.Constant{ + { + Data: []byte{0x2}, + Kind: constantkind.Int, + }, + { + Data: []byte{0x1}, + Kind: constantkind.Int, + }, + { + Data: []byte{0x2}, + 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 := NewCompiler(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.SetLocal), 0, 1, + // var fib2 = 1 + byte(opcode.GetConstant), 0, 1, + byte(opcode.SetLocal), 0, 2, + // var fibonacci = fib1 + byte(opcode.GetLocal), 0, 1, + byte(opcode.SetLocal), 0, 3, + // var i = 2 + byte(opcode.GetConstant), 0, 2, + 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, 69, + // fibonacci = fib1 + fib2 + byte(opcode.GetLocal), 0, 1, + byte(opcode.GetLocal), 0, 2, + byte(opcode.IntAdd), + byte(opcode.SetLocal), 0, 3, + // fib1 = fib2 + byte(opcode.GetLocal), 0, 2, + byte(opcode.SetLocal), 0, 1, + // fib2 = fibonacci + byte(opcode.GetLocal), 0, 3, + byte(opcode.SetLocal), 0, 2, + // i = i + 1 + byte(opcode.GetLocal), 0, 4, + byte(opcode.GetConstant), 0, 3, + byte(opcode.IntAdd), + byte(opcode.SetLocal), 0, 4, + // continue loop + byte(opcode.Jump), 0, 24, + // return fibonacci + byte(opcode.GetLocal), 0, 3, + byte(opcode.ReturnValue), + }, + compiler.functions[0].code, + ) + + require.Equal(t, + []*bbq.Constant{ + { + Data: []byte{0x1}, + Kind: constantkind.Int, + }, + { + Data: []byte{0x1}, + Kind: constantkind.Int, + }, + { + Data: []byte{0x2}, + Kind: constantkind.Int, + }, + { + Data: []byte{0x1}, + 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 := NewCompiler(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.SetLocal), 0, 0, + // while true + byte(opcode.True), + byte(opcode.JumpIfFalse), 0, 36, + // if i > 3 + byte(opcode.GetLocal), 0, 0, + byte(opcode.GetConstant), 0, 1, + byte(opcode.IntGreater), + byte(opcode.JumpIfFalse), 0, 23, + // break + byte(opcode.Jump), 0, 36, + // i = i + 1 + byte(opcode.GetLocal), 0, 0, + byte(opcode.GetConstant), 0, 2, + byte(opcode.IntAdd), + byte(opcode.SetLocal), 0, 0, + // repeat + byte(opcode.Jump), 0, 6, + // return i + byte(opcode.GetLocal), 0, 0, + byte(opcode.ReturnValue), + }, + compiler.functions[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 := NewCompiler(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.SetLocal), 0, 0, + // while true + byte(opcode.True), + byte(opcode.JumpIfFalse), 0, 39, + // i = i + 1 + byte(opcode.GetLocal), 0, 0, + byte(opcode.GetConstant), 0, 1, + byte(opcode.IntAdd), + 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, 33, + // continue + byte(opcode.Jump), 0, 6, + // break + byte(opcode.Jump), 0, 39, + // repeat + byte(opcode.Jump), 0, 6, + // return i + byte(opcode.GetLocal), 0, 0, + byte(opcode.ReturnValue), + }, + compiler.functions[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/runtime/bbq/compiler/constant.go b/runtime/bbq/compiler/constant.go new file mode 100644 index 0000000000..63984ccd1b --- /dev/null +++ b/runtime/bbq/compiler/constant.go @@ -0,0 +1,27 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright 2019-2022 Dapper Labs, Inc. + * + * 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/runtime/bbq/constantkind" + +type constant struct { + index uint16 + data []byte + kind constantkind.Constant +} diff --git a/runtime/bbq/compiler/function.go b/runtime/bbq/compiler/function.go new file mode 100644 index 0000000000..329cd32b2e --- /dev/null +++ b/runtime/bbq/compiler/function.go @@ -0,0 +1,66 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright 2019-2022 Dapper Labs, Inc. + * + * 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/runtime/activations" + "github.com/onflow/cadence/runtime/bbq/opcode" + "github.com/onflow/cadence/runtime/errors" +) + +type function struct { + name string + localCount uint16 + // TODO: use byte.Buffer? + code []byte + locals *activations.Activations[*local] + parameterCount uint16 +} + +func newFunction(name string, parameterCount uint16) *function { + return &function{ + name: name, + parameterCount: parameterCount, + locals: activations.NewActivations[*local](nil), + } +} + +func (f *function) emit(opcode opcode.Opcode, args ...byte) int { + offset := len(f.code) + f.code = append(f.code, byte(opcode)) + f.code = append(f.code, args...) + return offset +} + +func (f *function) 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) findLocal(name string) *local { + return f.locals.Find(name) +} diff --git a/runtime/bbq/compiler/global.go b/runtime/bbq/compiler/global.go new file mode 100644 index 0000000000..1fd2591ecc --- /dev/null +++ b/runtime/bbq/compiler/global.go @@ -0,0 +1,23 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright 2019-2022 Dapper Labs, Inc. + * + * 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 global struct { + index uint16 +} diff --git a/runtime/bbq/compiler/local.go b/runtime/bbq/compiler/local.go new file mode 100644 index 0000000000..30b467fbd5 --- /dev/null +++ b/runtime/bbq/compiler/local.go @@ -0,0 +1,23 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright 2019-2022 Dapper Labs, Inc. + * + * 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/runtime/bbq/compiler/loop.go b/runtime/bbq/compiler/loop.go new file mode 100644 index 0000000000..3fd4197f44 --- /dev/null +++ b/runtime/bbq/compiler/loop.go @@ -0,0 +1,24 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright 2019-2022 Dapper Labs, Inc. + * + * 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/runtime/bbq/constant.go b/runtime/bbq/constant.go new file mode 100644 index 0000000000..f90600b099 --- /dev/null +++ b/runtime/bbq/constant.go @@ -0,0 +1,26 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright 2019-2022 Dapper Labs, Inc. + * + * 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/runtime/bbq/constantkind" + +type Constant struct { + Data []byte + Kind constantkind.Constant +} diff --git a/runtime/bbq/constantkind/constantkind.go b/runtime/bbq/constantkind/constantkind.go new file mode 100644 index 0000000000..a5b6afd261 --- /dev/null +++ b/runtime/bbq/constantkind/constantkind.go @@ -0,0 +1,135 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright 2019-2022 Dapper Labs, Inc. + * + * 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/runtime/errors" + "github.com/onflow/cadence/runtime/sema" +) + +type Constant uint8 + +const ( + Unknown Constant = 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) Constant { + switch ty { + // Int* + case sema.IntType: + 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/runtime/bbq/function.go b/runtime/bbq/function.go new file mode 100644 index 0000000000..9d7686104e --- /dev/null +++ b/runtime/bbq/function.go @@ -0,0 +1,26 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright 2019-2022 Dapper Labs, Inc. + * + * 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 struct { + Name string + Code []byte + ParameterCount uint16 + LocalCount uint16 +} diff --git a/runtime/bbq/leb128/leb128.go b/runtime/bbq/leb128/leb128.go new file mode 100644 index 0000000000..735146a766 --- /dev/null +++ b/runtime/bbq/leb128/leb128.go @@ -0,0 +1,220 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright 2019-2022 Dapper Labs, Inc. + * + * 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/runtime/bbq/leb128/leb128_test.go b/runtime/bbq/leb128/leb128_test.go new file mode 100644 index 0000000000..b5a68dc29f --- /dev/null +++ b/runtime/bbq/leb128/leb128_test.go @@ -0,0 +1,292 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright 2019-2022 Dapper Labs, Inc. + * + * 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/runtime/bbq/opcode/opcode.go b/runtime/bbq/opcode/opcode.go new file mode 100644 index 0000000000..cba7d2aedc --- /dev/null +++ b/runtime/bbq/opcode/opcode.go @@ -0,0 +1,54 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright 2019-2022 Dapper Labs, Inc. + * + * 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 + + Return + ReturnValue + Jump + JumpIfFalse + + IntAdd + IntSubtract + IntMultiply + IntDivide + IntMod + IntEqual + IntNotEqual + IntLess + IntGreater + IntLessOrEqual + IntGreaterOrEqual + + GetConstant + True + False + + GetLocal + SetLocal + GetGlobal + + Call +) diff --git a/runtime/bbq/opcode/opcode_string.go b/runtime/bbq/opcode/opcode_string.go new file mode 100644 index 0000000000..d64bc27039 --- /dev/null +++ b/runtime/bbq/opcode/opcode_string.go @@ -0,0 +1,45 @@ +// 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-5] + _ = x[IntSubtract-6] + _ = x[IntMultiply-7] + _ = x[IntDivide-8] + _ = x[IntMod-9] + _ = x[IntEqual-10] + _ = x[IntNotEqual-11] + _ = x[IntLess-12] + _ = x[IntGreater-13] + _ = x[IntLessOrEqual-14] + _ = x[IntGreaterOrEqual-15] + _ = x[GetConstant-16] + _ = x[True-17] + _ = x[False-18] + _ = x[GetLocal-19] + _ = x[SetLocal-20] + _ = x[GetGlobal-21] + _ = x[Call-22] +} + +const _Opcode_name = "UnknownReturnReturnValueJumpJumpIfFalseIntAddIntSubtractIntMultiplyIntDivideIntModIntEqualIntNotEqualIntLessIntGreaterIntLessOrEqualIntGreaterOrEqualGetConstantTrueFalseGetLocalSetLocalGetGlobalCall" + +var _Opcode_index = [...]uint8{0, 7, 13, 24, 28, 39, 45, 56, 67, 76, 82, 90, 101, 108, 118, 132, 149, 160, 164, 169, 177, 185, 194, 198} + +func (i Opcode) String() string { + if i >= Opcode(len(_Opcode_index)-1) { + return "Opcode(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _Opcode_name[_Opcode_index[i]:_Opcode_index[i+1]] +} diff --git a/runtime/bbq/program.go b/runtime/bbq/program.go new file mode 100644 index 0000000000..90964f68ed --- /dev/null +++ b/runtime/bbq/program.go @@ -0,0 +1,24 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright 2019-2022 Dapper Labs, Inc. + * + * 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 struct { + Functions []*Function + Constants []*Constant +} diff --git a/runtime/bbq/vm/callframe.go b/runtime/bbq/vm/callframe.go new file mode 100644 index 0000000000..6bf88c405a --- /dev/null +++ b/runtime/bbq/vm/callframe.go @@ -0,0 +1,37 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright 2019-2022 Dapper Labs, Inc. + * + * 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/runtime/bbq" +) + +type callFrame struct { + parent *callFrame + locals []Value + function *bbq.Function + ip uint16 +} + +func (f *callFrame) getUint16() uint16 { + first := f.function.Code[f.ip] + last := f.function.Code[f.ip+1] + f.ip += 2 + return uint16(first)<<8 | uint16(last) +} diff --git a/runtime/bbq/vm/value.go b/runtime/bbq/vm/value.go new file mode 100644 index 0000000000..1821643b1e --- /dev/null +++ b/runtime/bbq/vm/value.go @@ -0,0 +1,74 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright 2019-2022 Dapper Labs, Inc. + * + * 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/runtime/bbq" +) + +type Value interface { + isValue() +} + +var trueValue Value = BoolValue(true) +var falseValue Value = BoolValue(false) + +type BoolValue bool + +var _ Value = BoolValue(true) + +func (BoolValue) isValue() {} + +type IntValue struct { + smallInt int64 +} + +var _ Value = IntValue{} + +func (IntValue) isValue() {} + +func (v IntValue) Add(other IntValue) Value { + return IntValue{v.smallInt + other.smallInt} +} + +func (v IntValue) Subtract(other IntValue) Value { + return IntValue{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 +} + +type FunctionValue struct { + Function *bbq.Function +} + +var _ Value = FunctionValue{} + +func (FunctionValue) isValue() {} diff --git a/runtime/bbq/vm/vm.go b/runtime/bbq/vm/vm.go new file mode 100644 index 0000000000..2be39c0509 --- /dev/null +++ b/runtime/bbq/vm/vm.go @@ -0,0 +1,314 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright 2019-2022 Dapper Labs, Inc. + * + * 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/runtime/bbq" + "github.com/onflow/cadence/runtime/bbq/constantkind" + "github.com/onflow/cadence/runtime/bbq/leb128" + "github.com/onflow/cadence/runtime/bbq/opcode" + "github.com/onflow/cadence/runtime/errors" +) + +type VM struct { + Program *bbq.Program + globals []Value + constants []Value + functions map[string]*bbq.Function + callFrame *callFrame + stack []Value +} + +func NewVM(program *bbq.Program) *VM { + functions := indexFunctions(program.Functions) + + // TODO: include non-function globals + globals := make([]Value, 0, len(functions)) + for _, function := range functions { + // TODO: + globals = append(globals, FunctionValue{Function: function}) + } + + return &VM{ + Program: program, + globals: globals, + functions: functions, + constants: make([]Value, len(program.Constants)), + } +} + +func indexFunctions(functions []*bbq.Function) map[string]*bbq.Function { + indexedFunctions := make(map[string]*bbq.Function, len(functions)) + for _, function := range functions { + indexedFunctions[function.Name] = function + } + return indexedFunctions +} + +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] + return value +} + +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 (vm *VM) pushCallFrame(function *bbq.Function, arguments []Value) { + + locals := make([]Value, function.LocalCount) + for i, argument := range arguments { + locals[i] = argument + } + + callFrame := &callFrame{ + parent: vm.callFrame, + locals: locals, + function: function, + } + vm.callFrame = callFrame +} + +func (vm *VM) popCallFrame() { + vm.callFrame = vm.callFrame.parent +} + +func (vm *VM) Invoke(name string, arguments ...Value) (Value, error) { + function, ok := vm.functions[name] + if !ok { + return nil, errors.NewDefaultUserError("unknown function") + } + + if len(arguments) != int(function.ParameterCount) { + return nil, errors.NewDefaultUserError("wrong number of arguments") + } + + vm.pushCallFrame(function, arguments) + + vm.run() + + if len(vm.stack) == 0 { + return nil, nil + } + + return vm.pop(), nil +} + +type vmOp func(*VM) + +var vmOps = [...]vmOp{ + opcode.ReturnValue: opReturnValue, + opcode.Jump: opJump, + opcode.JumpIfFalse: opJumpIfFalse, + opcode.IntAdd: opBinaryIntAdd, + opcode.IntSubtract: opBinaryIntSubtract, + opcode.IntLess: opBinaryIntLess, + opcode.IntGreater: opBinaryIntGreater, + opcode.True: opTrue, + opcode.False: opFalse, + opcode.GetConstant: opGetConstant, + opcode.GetLocal: opGetLocal, + opcode.SetLocal: opSetLocal, + opcode.GetGlobal: opGetGlobal, + opcode.Call: opCall, +} + +func opReturnValue(vm *VM) { + value := vm.pop() + vm.popCallFrame() + vm.push(value) +} + +func opJump(vm *VM) { + callFrame := vm.callFrame + target := callFrame.getUint16() + callFrame.ip = target +} + +func opJumpIfFalse(vm *VM) { + callFrame := vm.callFrame + target := callFrame.getUint16() + value := vm.pop().(BoolValue) + if !value { + callFrame.ip = 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) { + callFrame := vm.callFrame + index := callFrame.getUint16() + constant := vm.constants[index] + if constant == nil { + constant = vm.initializeConstant(index) + } + vm.push(constant) +} + +func opGetLocal(vm *VM) { + callFrame := vm.callFrame + index := callFrame.getUint16() + local := callFrame.locals[index] + vm.push(local) +} + +func opSetLocal(vm *VM) { + callFrame := vm.callFrame + index := callFrame.getUint16() + callFrame.locals[index] = vm.pop() +} + +func opGetGlobal(vm *VM) { + callFrame := vm.callFrame + index := callFrame.getUint16() + vm.push(vm.globals[index]) +} + +func opCall(vm *VM) { + // TODO: support any function value + value := vm.pop().(FunctionValue) + stackHeight := len(vm.stack) + parameterCount := int(value.Function.ParameterCount) + arguments := vm.stack[stackHeight-parameterCount:] + vm.pushCallFrame(value.Function, arguments) + vm.dropN(parameterCount) +} + +func (vm *VM) run() { + for { + + callFrame := vm.callFrame + + if callFrame == nil || + int(callFrame.ip) >= len(callFrame.function.Code) { + + return + } + + op := opcode.Opcode(callFrame.function.Code[callFrame.ip]) + callFrame.ip++ + + switch op { + case opcode.ReturnValue: + opReturnValue(vm) + case opcode.Jump: + opJump(vm) + case opcode.JumpIfFalse: + opJumpIfFalse(vm) + case opcode.IntAdd: + opBinaryIntAdd(vm) + case opcode.IntSubtract: + opBinaryIntSubtract(vm) + case opcode.IntLess: + opBinaryIntLess(vm) + case opcode.IntGreater: + opBinaryIntGreater(vm) + case opcode.True: + opTrue(vm) + case opcode.False: + opFalse(vm) + case opcode.GetConstant: + opGetConstant(vm) + case opcode.GetLocal: + opGetLocal(vm) + case opcode.SetLocal: + opSetLocal(vm) + case opcode.GetGlobal: + opGetGlobal(vm) + case opcode.Call: + opCall(vm) + default: + panic(errors.NewUnreachableError()) + } + + // Faster in Go <1.19: + // vmOps[op](vm) + } +} + +func (vm *VM) initializeConstant(index uint16) (value Value) { + constant := vm.Program.Constants[index] + switch constant.Kind { + case constantkind.Int: + // TODO: + smallInt, _, _ := leb128.ReadInt64(constant.Data) + value = IntValue{smallInt} + default: + // TODO: + panic(errors.NewUnreachableError()) + } + vm.constants[index] = value + return value +} diff --git a/runtime/bbq/vm/vm_test.go b/runtime/bbq/vm/vm_test.go new file mode 100644 index 0000000000..4d165d5d95 --- /dev/null +++ b/runtime/bbq/vm/vm_test.go @@ -0,0 +1,199 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright 2019-2022 Dapper Labs, Inc. + * + * 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 ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/onflow/cadence/runtime/bbq/compiler" + . "github.com/onflow/cadence/runtime/tests/checker" +) + +const recursiveFib = ` + fun fib(_ n: Int): Int { + if n < 2 { + return n + } + return fib(n - 1) + fib(n - 2) + } +` + +func TestRecursionFib(t *testing.T) { + + t.Parallel() + + checker, err := ParseAndCheck(t, recursiveFib) + require.NoError(t, err) + + comp := compiler.NewCompiler(checker.Program, checker.Elaboration) + program := comp.Compile() + + vm := NewVM(program) + + result, err := vm.Invoke( + "fib", + IntValue{7}, + ) + require.NoError(t, err) + require.Equal(t, IntValue{13}, result) +} + +func BenchmarkRecursionFib(b *testing.B) { + + checker, err := ParseAndCheck(b, recursiveFib) + require.NoError(b, err) + + comp := compiler.NewCompiler(checker.Program, checker.Elaboration) + program := comp.Compile() + + vm := NewVM(program) + + b.ReportAllocs() + b.ResetTimer() + + expected := IntValue{377} + + for i := 0; i < b.N; i++ { + + result, err := vm.Invoke( + "fib", + IntValue{14}, + ) + require.NoError(b, err) + require.Equal(b, expected, result) + } +} + +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.NewCompiler(checker.Program, checker.Elaboration) + program := comp.Compile() + + vm := NewVM(program) + + result, err := vm.Invoke( + "fib", + IntValue{7}, + ) + require.NoError(t, err) + require.Equal(t, IntValue{13}, result) +} + +func BenchmarkImperativeFib(b *testing.B) { + + checker, err := ParseAndCheck(b, imperativeFib) + require.NoError(b, err) + + comp := compiler.NewCompiler(checker.Program, checker.Elaboration) + program := comp.Compile() + + vm := NewVM(program) + + b.ReportAllocs() + b.ResetTimer() + + var value Value = IntValue{14} + + for i := 0; i < b.N; i++ { + _, err := vm.Invoke("fib", value) + require.NoError(b, err) + } +} + +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.NewCompiler(checker.Program, checker.Elaboration) + program := comp.Compile() + + vm := NewVM(program) + + result, err := vm.Invoke("test") + require.NoError(t, err) + + require.Equal(t, IntValue{4}, result) +} + +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.NewCompiler(checker.Program, checker.Elaboration) + program := comp.Compile() + + vm := NewVM(program) + + result, err := vm.Invoke("test") + require.NoError(t, err) + + require.Equal(t, IntValue{3}, result) +} diff --git a/runtime/tests/interpreter/fib_test.go b/runtime/tests/interpreter/fib_test.go new file mode 100644 index 0000000000..009b7b584a --- /dev/null +++ b/runtime/tests/interpreter/fib_test.go @@ -0,0 +1,71 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright 2019-2022 Dapper Labs, Inc. + * + * 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 interpreter_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/onflow/cadence/runtime/interpreter" +) + +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() + + inter := parseCheckAndInterpret(t, imperativeFib) + + 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 BenchmarkImperativeFib(b *testing.B) { + + inter := parseCheckAndInterpret(b, imperativeFib) + + 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) + } +}