From 39b3351560a5139d2d84ffb549b43fe1c69076ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Thu, 13 Feb 2025 14:35:12 -0800 Subject: [PATCH 1/3] compile and execute emit statement --- bbq/compiler/compiler.go | 12 ++++++-- bbq/compiler/compiler_test.go | 41 ++++++++++++++++++++++++++++ bbq/compiler/extended_elaboration.go | 4 +++ bbq/opcode/instructions.go | 32 ++++++++++++++++++++++ bbq/opcode/instructions.yml | 13 +++++++++ bbq/opcode/opcode.go | 6 ++++ bbq/vm/config.go | 9 ++++++ bbq/vm/test/vm_test.go | 38 ++++++++++++++++++++++++++ bbq/vm/vm.go | 17 ++++++++++++ 9 files changed, 169 insertions(+), 3 deletions(-) diff --git a/bbq/compiler/compiler.go b/bbq/compiler/compiler.go index d5250a240..6cacdae00 100644 --- a/bbq/compiler/compiler.go +++ b/bbq/compiler/compiler.go @@ -634,9 +634,15 @@ func (c *Compiler[_]) VisitForStatement(_ *ast.ForStatement) (_ struct{}) { panic(errors.NewUnreachableError()) } -func (c *Compiler[_]) VisitEmitStatement(_ *ast.EmitStatement) (_ struct{}) { - // TODO - panic(errors.NewUnreachableError()) +func (c *Compiler[_]) VisitEmitStatement(statement *ast.EmitStatement) (_ struct{}) { + c.compileExpression(statement.InvocationExpression) + eventType := c.ExtendedElaboration.EmitStatementEventType(statement) + typeIndex := c.getOrAddType(eventType) + c.codeGen.Emit(opcode.InstructionEmitEvent{ + TypeIndex: typeIndex, + }) + + return } func (c *Compiler[_]) VisitSwitchStatement(statement *ast.SwitchStatement) (_ struct{}) { diff --git a/bbq/compiler/compiler_test.go b/bbq/compiler/compiler_test.go index 64850c4b6..42c5f813f 100644 --- a/bbq/compiler/compiler_test.go +++ b/bbq/compiler/compiler_test.go @@ -654,3 +654,44 @@ func TestCompileSwitch(t *testing.T) { program.Constants, ) } + +func TestCompileEmit(t *testing.T) { + + t.Parallel() + + checker, err := ParseAndCheck(t, ` + event Inc(val: Int) + + fun test(x: Int) { + emit Inc(val: x) + } + `) + require.NoError(t, err) + + compiler := NewInstructionCompiler(checker) + program := compiler.Compile() + + require.Len(t, program.Functions, 2) + + var testFunction *bbq.Function[opcode.Instruction] + for _, f := range compiler.ExportFunctions() { + if f.Name == "test" { + testFunction = f + } + } + require.NotNil(t, testFunction) + + assert.Equal(t, + []opcode.Instruction{ + opcode.InstructionGetLocal{LocalIndex: 0}, + opcode.InstructionTransfer{TypeIndex: 0}, + opcode.InstructionGetGlobal{GlobalIndex: 1}, + opcode.InstructionInvoke{}, + opcode.InstructionEmitEvent{TypeIndex: 1}, + opcode.InstructionReturn{}, + }, + testFunction.Code, + ) + + assert.Empty(t, program.Constants) +} diff --git a/bbq/compiler/extended_elaboration.go b/bbq/compiler/extended_elaboration.go index ff5b4f308..68ef5abd5 100644 --- a/bbq/compiler/extended_elaboration.go +++ b/bbq/compiler/extended_elaboration.go @@ -230,6 +230,10 @@ func (e *ExtendedElaboration) CastingExpressionTypes(expression *ast.CastingExpr return e.elaboration.CastingExpressionTypes(expression) } +func (e *ExtendedElaboration) EmitStatementEventType(statement *ast.EmitStatement) *sema.CompositeType { + return e.elaboration.EmitStatementEventType(statement) +} + func (e *ExtendedElaboration) ResultVariableType(enclosingBlock ast.Element) (typ sema.Type, exist bool) { if e.resultVariableTypes != nil { types, ok := e.resultVariableTypes[enclosingBlock] diff --git a/bbq/opcode/instructions.go b/bbq/opcode/instructions.go index e6bf8a98a..96a7dac5b 100644 --- a/bbq/opcode/instructions.go +++ b/bbq/opcode/instructions.go @@ -1095,6 +1095,36 @@ func (i InstructionGreaterOrEqual) Encode(code *[]byte) { emitOpcode(code, i.Opcode()) } +// InstructionEmitEvent +// +// Pops an event off the stack and then emits it. +type InstructionEmitEvent struct { + TypeIndex uint16 +} + +var _ Instruction = InstructionEmitEvent{} + +func (InstructionEmitEvent) Opcode() Opcode { + return EmitEvent +} + +func (i InstructionEmitEvent) String() string { + var sb strings.Builder + sb.WriteString(i.Opcode().String()) + printfArgument(&sb, "typeIndex", i.TypeIndex) + return sb.String() +} + +func (i InstructionEmitEvent) Encode(code *[]byte) { + emitOpcode(code, i.Opcode()) + emitUint16(code, i.TypeIndex) +} + +func DecodeEmitEvent(ip *uint16, code []byte) (i InstructionEmitEvent) { + i.TypeIndex = decodeUint16(ip, code) + return i +} + func DecodeInstruction(ip *uint16, code []byte) Instruction { switch Opcode(decodeByte(ip, code)) { case Unknown: @@ -1183,6 +1213,8 @@ func DecodeInstruction(ip *uint16, code []byte) Instruction { return InstructionGreater{} case GreaterOrEqual: return InstructionGreaterOrEqual{} + case EmitEvent: + return DecodeEmitEvent(ip, code) } panic(errors.NewUnreachableError()) diff --git a/bbq/opcode/instructions.yml b/bbq/opcode/instructions.yml index 90e80fcab..87bdd8ecc 100644 --- a/bbq/opcode/instructions.yml +++ b/bbq/opcode/instructions.yml @@ -572,3 +572,16 @@ push: - name: "result" type: "bool" + +# Other + +- name: "emitEvent" + description: + Pops an event off the stack and then emits it. + operands: + - name: "typeIndex" + type: "index" + valueEffects: + pop: + - name: "event" + type: "value" diff --git a/bbq/opcode/opcode.go b/bbq/opcode/opcode.go index 772349f60..d36b211ff 100644 --- a/bbq/opcode/opcode.go +++ b/bbq/opcode/opcode.go @@ -142,4 +142,10 @@ const ( _ _ _ + _ + _ + _ + + // Other + EmitEvent ) diff --git a/bbq/vm/config.go b/bbq/vm/config.go index 128f100ce..da4ba3119 100644 --- a/bbq/vm/config.go +++ b/bbq/vm/config.go @@ -27,6 +27,12 @@ import ( "github.com/onflow/cadence/test_utils/common_utils" ) +// OnEventEmittedFunc is a function that is triggered when an event is emitted by the program. +type OnEventEmittedFunc func( + event *CompositeValue, + eventType *interpreter.CompositeStaticType, +) error + type Config struct { interpreter.Storage common.MemoryGauge @@ -41,6 +47,9 @@ type Config struct { MutationDuringCapabilityControllerIteration bool referencedResourceKindedValues ReferencedResourceKindedValues + // OnEventEmitted is triggered when an event is emitted by the program + OnEventEmitted OnEventEmittedFunc + // 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 diff --git a/bbq/vm/test/vm_test.go b/bbq/vm/test/vm_test.go index ebcb17060..6f84c7ca2 100644 --- a/bbq/vm/test/vm_test.go +++ b/bbq/vm/test/vm_test.go @@ -3645,3 +3645,41 @@ func TestDefaultFunctionsWithConditions(t *testing.T) { ) }) } + +func TestCompileEmit(t *testing.T) { + + t.Parallel() + + var eventEmitted bool + + vmConfig := vm.NewConfig(interpreter.NewInMemoryStorage(nil)) + vmConfig.OnEventEmitted = func(event *vm.CompositeValue, eventType *interpreter.CompositeStaticType) error { + require.False(t, eventEmitted) + eventEmitted = true + + assert.Equal(t, + common.ScriptLocation{0x1}.TypeID(nil, "Inc"), + eventType.ID(), + ) + + return nil + } + + _, err := compileAndInvokeWithOptions(t, + ` + event Inc(val: Int) + + fun test(x: Int) { + emit Inc(val: x) + } + `, + "test", + CompilerAndVMOptions{ + VMConfig: vmConfig, + }, + vm.NewIntValue(1), + ) + require.NoError(t, err) + + require.True(t, eventEmitted) +} diff --git a/bbq/vm/vm.go b/bbq/vm/vm.go index 181855abe..73842e827 100644 --- a/bbq/vm/vm.go +++ b/bbq/vm/vm.go @@ -804,6 +804,8 @@ func (vm *VM) run() { opNot(vm) case opcode.InstructionUnwrap: opUnwrap(vm) + case opcode.InstructionEmitEvent: + onEmitEvent(vm, ins) default: panic(errors.NewUnexpectedError("cannot execute instruction of type %T", ins)) } @@ -813,6 +815,21 @@ func (vm *VM) run() { } } +func onEmitEvent(vm *VM, ins opcode.InstructionEmitEvent) { + eventType := vm.loadType(ins.TypeIndex).(*interpreter.CompositeStaticType) + eventValue := vm.pop().(*CompositeValue) + + onEventEmitted := vm.config.OnEventEmitted + if onEventEmitted == nil { + return + } + + err := onEventEmitted(eventValue, eventType) + if err != nil { + panic(err) + } +} + func (vm *VM) initializeConstant(index uint16) (value Value) { executable := vm.callFrame.executable From 9ee137fd6056cdf0edbc7e53094f16a08f8073ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Thu, 13 Feb 2025 14:55:10 -0800 Subject: [PATCH 2/3] only load type if needed --- bbq/vm/vm.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bbq/vm/vm.go b/bbq/vm/vm.go index 73842e827..0eed204f1 100644 --- a/bbq/vm/vm.go +++ b/bbq/vm/vm.go @@ -816,7 +816,6 @@ func (vm *VM) run() { } func onEmitEvent(vm *VM, ins opcode.InstructionEmitEvent) { - eventType := vm.loadType(ins.TypeIndex).(*interpreter.CompositeStaticType) eventValue := vm.pop().(*CompositeValue) onEventEmitted := vm.config.OnEventEmitted @@ -824,6 +823,8 @@ func onEmitEvent(vm *VM, ins opcode.InstructionEmitEvent) { return } + eventType := vm.loadType(ins.TypeIndex).(*interpreter.CompositeStaticType) + err := onEventEmitted(eventValue, eventType) if err != nil { panic(err) From 101c35fa2407a9bfe4d00313c559bffe1d569364 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Fri, 14 Feb 2025 08:19:05 -0800 Subject: [PATCH 3/3] generate Opcode.String() --- bbq/opcode/opcode_string.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bbq/opcode/opcode_string.go b/bbq/opcode/opcode_string.go index 58f99e256..68dd1f309 100644 --- a/bbq/opcode/opcode_string.go +++ b/bbq/opcode/opcode_string.go @@ -51,6 +51,7 @@ func _() { _ = x[InvokeDynamic-86] _ = x[Drop-95] _ = x[Dup-96] + _ = x[EmitEvent-103] } const ( @@ -63,6 +64,7 @@ const ( _Opcode_name_6 = "GetConstantGetLocalSetLocalGetGlobalSetGlobalGetFieldSetFieldSetIndexGetIndex" _Opcode_name_7 = "InvokeInvokeDynamic" _Opcode_name_8 = "DropDup" + _Opcode_name_9 = "EmitEvent" ) var ( @@ -105,6 +107,8 @@ func (i Opcode) String() string { case 95 <= i && i <= 96: i -= 95 return _Opcode_name_8[_Opcode_index_8[i]:_Opcode_index_8[i+1]] + case i == 103: + return _Opcode_name_9 default: return "Opcode(" + strconv.FormatInt(int64(i), 10) + ")" }