From 3dc7e24b867e4042e4c383cf1e68f7c65cc792d8 Mon Sep 17 00:00:00 2001 From: Antoine Gelloz Date: Mon, 24 Apr 2023 10:51:33 +0200 Subject: [PATCH] feat(Numscript): Send monetary with arithmetic (#215) --- pkg/core/value.go | 2 +- pkg/machine/script/compiler/compiler.go | 231 +++++++++++++------ pkg/machine/script/compiler/compiler_test.go | 129 +++++++++++ pkg/machine/script/compiler/source.go | 93 ++------ pkg/machine/vm/machine.go | 61 +++-- pkg/machine/vm/machine_test.go | 85 +++++++ pkg/machine/vm/program/instructions.go | 9 +- 7 files changed, 445 insertions(+), 165 deletions(-) diff --git a/pkg/core/value.go b/pkg/core/value.go index 6461b552b..d5962a50f 100644 --- a/pkg/core/value.go +++ b/pkg/core/value.go @@ -13,7 +13,7 @@ const ( TypeNumber // 64bit unsigned integer TypeString // string TypeMonetary // [asset number] - TypePortion // rational number between 0 and 1 both exclusive + TypePortion // rational number between 0 and 1 both inclusive TypeAllotment // list of portions TypeAmount // either ALL or a SPECIFIC number TypeFunding // (asset, []{amount, account}) diff --git a/pkg/machine/script/compiler/compiler.go b/pkg/machine/script/compiler/compiler.go index 8cd88af60..bd6cae15d 100644 --- a/pkg/machine/script/compiler/compiler.go +++ b/pkg/machine/script/compiler/compiler.go @@ -78,38 +78,58 @@ func (p *parseVisitor) VisitVariable(c parser.IVariableContext, push bool) (core func (p *parseVisitor) VisitExpr(c parser.IExpressionContext, push bool) (core.Type, *core.Address, *CompileError) { switch c := c.(type) { case *parser.ExprAddSubContext: - ty, _, err := p.VisitExpr(c.GetLhs(), push) + lhsType, lhsAddr, err := p.VisitExpr(c.GetLhs(), push) if err != nil { return 0, nil, err } - if ty != core.TypeNumber { - return 0, nil, LogicError(c, errors.New("tried to do arithmetic with wrong type")) - } - ty, _, err = p.VisitExpr(c.GetRhs(), push) - if err != nil { - return 0, nil, err - } - if ty != core.TypeNumber { - return 0, nil, LogicError(c, errors.New("tried to do arithmetic with wrong type")) - } - if push { - switch c.GetOp().GetTokenType() { - case parser.NumScriptLexerOP_ADD: - p.AppendInstruction(program.OP_IADD) - case parser.NumScriptLexerOP_SUB: - p.AppendInstruction(program.OP_ISUB) + switch lhsType { + case core.TypeNumber: + rhsType, _, err := p.VisitExpr(c.GetRhs(), push) + if err != nil { + return 0, nil, err + } + if rhsType != core.TypeNumber { + return 0, nil, LogicError(c, fmt.Errorf( + "tried to do an arithmetic operation with incompatible left and right-hand side operand types: %s and %s", + lhsType, rhsType)) + } + if push { + switch c.GetOp().GetTokenType() { + case parser.NumScriptLexerOP_ADD: + p.AppendInstruction(program.OP_IADD) + case parser.NumScriptLexerOP_SUB: + p.AppendInstruction(program.OP_ISUB) + } + } + return core.TypeNumber, nil, nil + case core.TypeMonetary: + rhsType, _, err := p.VisitExpr(c.GetRhs(), push) + if err != nil { + return 0, nil, err } + if rhsType != core.TypeMonetary { + return 0, nil, LogicError(c, fmt.Errorf( + "tried to do an arithmetic operation with incompatible left and right-hand side operand types: %s and %s", + lhsType, rhsType)) + } + if push { + switch c.GetOp().GetTokenType() { + case parser.NumScriptLexerOP_ADD: + p.AppendInstruction(program.OP_MONETARY_ADD) + case parser.NumScriptLexerOP_SUB: + p.AppendInstruction(program.OP_MONETARY_SUB) + } + } + return core.TypeMonetary, lhsAddr, nil + default: + return 0, nil, LogicError(c, fmt.Errorf( + "tried to do an arithmetic operation with unsupported left-hand side operand type: %s", + lhsType)) } - return core.TypeNumber, nil, nil case *parser.ExprLiteralContext: - ty, addr, err := p.VisitLit(c.GetLit(), push) - if err != nil { - return 0, nil, err - } - return ty, addr, nil + return p.VisitLit(c.GetLit(), push) case *parser.ExprVariableContext: - ty, addr, err := p.VisitVariable(c.GetVar_(), push) - return ty, addr, err + return p.VisitVariable(c.GetVar_(), push) default: return 0, nil, InternalError(c) } @@ -142,13 +162,14 @@ func (p *parseVisitor) VisitLit(c parser.ILiteralContext, push bool) (core.Type, if err != nil { return 0, nil, LogicError(c, err) } + addr, err := p.AllocateResource(program.Constant{Inner: number}) + if err != nil { + return 0, nil, LogicError(c, err) + } if push { - err := p.PushInteger(number) - if err != nil { - return 0, nil, LogicError(c, err) - } + p.PushAddress(*addr) } - return core.TypeNumber, nil, nil + return core.TypeNumber, addr, nil case *parser.LitStringContext: addr, err := p.AllocateResource(program.Constant{ Inner: core.String(strings.Trim(c.GetText(), `"`)), @@ -183,16 +204,35 @@ func (p *parseVisitor) VisitLit(c parser.ILiteralContext, push bool) (core.Type, "the expression in monetary literal should be of type '%s' instead of '%s'", core.TypeAsset, typ)) } + amt, err := core.ParseMonetaryInt(c.Monetary().GetAmt().GetText()) if err != nil { return 0, nil, LogicError(c, err) } - monAddr, err := p.AllocateResource(program.Monetary{ - Asset: *assetAddr, - Amount: amt, - }) - if err != nil { - return 0, nil, LogicError(c, err) + + var ( + monAddr *core.Address + alreadyAllocated bool + ) + for i, r := range p.resources { + switch v := r.(type) { + case program.Monetary: + if v.Asset == *assetAddr && v.Amount.Equal(amt) { + alreadyAllocated = true + tmp := core.Address(uint16(i)) + monAddr = &tmp + break + } + } + } + if !alreadyAllocated { + monAddr, err = p.AllocateResource(program.Monetary{ + Asset: *assetAddr, + Amount: amt, + }) + if err != nil { + return 0, nil, LogicError(c, err) + } } if push { p.PushAddress(*monAddr) @@ -203,49 +243,98 @@ func (p *parseVisitor) VisitLit(c parser.ILiteralContext, push bool) (core.Type, } } -func (p *parseVisitor) VisitSend(c *parser.SendContext) *CompileError { - var ( - accounts map[core.Address]struct{} - addr *core.Address - compErr *CompileError - typ core.Type - ) +func (p *parseVisitor) VisitMonetaryAll(c *parser.SendContext, monAll parser.IMonetaryAllContext) *CompileError { + assetType, assetAddr, compErr := p.VisitExpr(monAll.GetAsset(), false) + if compErr != nil { + return compErr + } + if assetType != core.TypeAsset { + return LogicError(c, fmt.Errorf( + "send monetary all: the expression should be of type 'asset' instead of '%s'", assetType)) + } - if monAll := c.GetMonAll(); monAll != nil { - typ, addr, compErr = p.VisitExpr(monAll.GetAsset(), false) + switch c := c.GetSrc().(type) { + case *parser.SrcContext: + accounts, _, _, compErr := p.VisitSource(c.Source(), func() { + p.PushAddress(*assetAddr) + }, true) if compErr != nil { return compErr } - if typ != core.TypeAsset { - return LogicError(c, fmt.Errorf( - "send monetary all: the expression should be of type 'asset' instead of '%s'", typ)) - } + p.setNeededBalances(accounts, assetAddr) - accounts, compErr = p.VisitValueAwareSource(c.GetSrc(), func() { - p.PushAddress(*addr) - }, nil) + case *parser.SrcAllotmentContext: + return LogicError(c, errors.New("cannot take all balance of an allotment source")) + } + return nil +} + +func (p *parseVisitor) VisitMonetary(c *parser.SendContext, mon parser.IExpressionContext) *CompileError { + monType, monAddr, compErr := p.VisitExpr(mon, false) + if compErr != nil { + return compErr + } + if monType != core.TypeMonetary { + return LogicError(c, fmt.Errorf( + "send monetary: the expression should be of type 'monetary' instead of '%s'", monType)) + } + + switch c := c.GetSrc().(type) { + case *parser.SrcContext: + accounts, _, fallback, compErr := p.VisitSource(c.Source(), func() { + p.PushAddress(*monAddr) + p.AppendInstruction(program.OP_ASSET) + }, false) if compErr != nil { return compErr } - } else if mon := c.GetMon(); mon != nil { - typ, addr, compErr = p.VisitExpr(mon, false) - if compErr != nil { - return compErr + p.setNeededBalances(accounts, monAddr) + + if _, _, err := p.VisitExpr(mon, true); err != nil { + return err } - if typ != core.TypeMonetary { - return LogicError(c, fmt.Errorf( - "send monetary: the expression should be of type 'monetary' instead of '%s'", typ)) + + if err := p.TakeFromSource(fallback); err != nil { + return LogicError(c, err) } + case *parser.SrcAllotmentContext: + if _, _, err := p.VisitExpr(mon, true); err != nil { + return err + } + p.VisitAllotment(c.SourceAllotment(), c.SourceAllotment().GetPortions()) + p.AppendInstruction(program.OP_ALLOC) - accounts, compErr = p.VisitValueAwareSource(c.GetSrc(), func() { - p.PushAddress(*addr) - p.AppendInstruction(program.OP_ASSET) - }, addr) - if compErr != nil { - return compErr + sources := c.SourceAllotment().GetSources() + n := len(sources) + for i := 0; i < n; i++ { + accounts, _, fallback, compErr := p.VisitSource(sources[i], func() { + p.PushAddress(*monAddr) + p.AppendInstruction(program.OP_ASSET) + }, false) + if compErr != nil { + return compErr + } + p.setNeededBalances(accounts, monAddr) + + if err := p.Bump(int64(i + 1)); err != nil { + return LogicError(c, err) + } + + if err := p.TakeFromSource(fallback); err != nil { + return LogicError(c, err) + } + } + + if err := p.PushInteger(core.NewNumber(int64(n))); err != nil { + return LogicError(c, err) } + + p.AppendInstruction(program.OP_FUNDING_ASSEMBLE) } + return nil +} +func (p *parseVisitor) setNeededBalances(accounts map[core.Address]struct{}, addr *core.Address) { for acc := range accounts { if b, ok := p.neededBalances[acc]; ok { b[*addr] = struct{}{} @@ -255,6 +344,18 @@ func (p *parseVisitor) VisitSend(c *parser.SendContext) *CompileError { } } } +} + +func (p *parseVisitor) VisitSend(c *parser.SendContext) *CompileError { + if monAll := c.GetMonAll(); monAll != nil { + if err := p.VisitMonetaryAll(c, monAll); err != nil { + return err + } + } else if mon := c.GetMon(); mon != nil { + if err := p.VisitMonetary(c, mon); err != nil { + return err + } + } if err := p.VisitDestination(c.GetDest()); err != nil { return err diff --git a/pkg/machine/script/compiler/compiler_test.go b/pkg/machine/script/compiler/compiler_test.go index 4a7475479..6852131ff 100644 --- a/pkg/machine/script/compiler/compiler_test.go +++ b/pkg/machine/script/compiler/compiler_test.go @@ -1351,3 +1351,132 @@ func TestVariableAsset(t *testing.T) { }, }) } + +func TestPrint(t *testing.T) { + script := `print 1 + 2 + 3` + test(t, TestCase{ + Case: script, + Expected: CaseResult{ + Instructions: []byte{ + program.OP_APUSH, 00, 00, + program.OP_APUSH, 01, 00, + program.OP_IADD, + program.OP_APUSH, 02, 00, + program.OP_IADD, + program.OP_PRINT, + }, + Resources: []program.Resource{ + program.Constant{Inner: core.NewMonetaryInt(1)}, + program.Constant{Inner: core.NewMonetaryInt(2)}, + program.Constant{Inner: core.NewMonetaryInt(3)}, + }, + }, + }) +} + +func TestSendWithArithmetic(t *testing.T) { + t.Run("nominal", func(t *testing.T) { + script := ` + vars { + asset $ass + monetary $mon + } + send [EUR 1] + $mon + [$ass 3] - [EUR 4] ( + source = @a + destination = @b + )` + + test(t, TestCase{ + Case: script, + Expected: CaseResult{ + Instructions: []byte{ + program.OP_APUSH, 06, 00, + program.OP_APUSH, 03, 00, + program.OP_ASSET, + program.OP_APUSH, 07, 00, + program.OP_MONETARY_NEW, + program.OP_TAKE_ALL, + program.OP_APUSH, 03, 00, + program.OP_APUSH, 01, 00, + program.OP_MONETARY_ADD, + program.OP_APUSH, 04, 00, + program.OP_MONETARY_ADD, + program.OP_APUSH, 05, 00, + program.OP_MONETARY_SUB, + program.OP_TAKE, + program.OP_APUSH, 8, 00, + program.OP_BUMP, + program.OP_REPAY, + program.OP_FUNDING_SUM, + program.OP_TAKE, + program.OP_APUSH, 9, 00, + program.OP_SEND, + program.OP_REPAY, + }, + Resources: []program.Resource{ + program.Variable{ + Typ: core.TypeAsset, + Name: "ass", + }, + program.Variable{ + Typ: core.TypeMonetary, + Name: "mon", + }, + program.Constant{Inner: core.Asset("EUR")}, + program.Monetary{ + Asset: 2, + Amount: core.NewMonetaryInt(1), + }, + program.Monetary{ + Asset: 0, + Amount: core.NewMonetaryInt(3), + }, + program.Monetary{ + Asset: 2, + Amount: core.NewMonetaryInt(4), + }, + program.Constant{Inner: core.AccountAddress("a")}, + program.Constant{Inner: core.NewMonetaryInt(0)}, + program.Constant{Inner: core.NewMonetaryInt(1)}, + program.Constant{Inner: core.AccountAddress("b")}, + }, + }, + }) + }) + + t.Run("error incompatible types", func(t *testing.T) { + script := `send [EUR 1] + 2 ( + source = @world + destination = @bob + )` + + test(t, TestCase{ + Case: script, + Expected: CaseResult{ + Instructions: []byte{}, + Resources: []program.Resource{}, + Error: "tried to do an arithmetic operation with incompatible left and right-hand side operand types: monetary and number", + }, + }) + }) + + t.Run("error incompatible types var", func(t *testing.T) { + script := ` + vars { + number $nb + } + send [EUR 1] - $nb ( + source = @world + destination = @bob + )` + + test(t, TestCase{ + Case: script, + Expected: CaseResult{ + Instructions: []byte{}, + Resources: []program.Resource{}, + Error: "tried to do an arithmetic operation with incompatible left and right-hand side operand types: monetary and number", + }, + }) + }) +} diff --git a/pkg/machine/script/compiler/source.go b/pkg/machine/script/compiler/source.go index dc793601d..17767dac3 100644 --- a/pkg/machine/script/compiler/source.go +++ b/pkg/machine/script/compiler/source.go @@ -11,62 +11,6 @@ import ( type FallbackAccount core.Address -// VisitValueAwareSource returns the resource addresses of all the accounts -func (p *parseVisitor) VisitValueAwareSource(c parser.IValueAwareSourceContext, pushAsset func(), monAddr *core.Address) (map[core.Address]struct{}, *CompileError) { - neededAccounts := map[core.Address]struct{}{} - isAll := monAddr == nil - switch c := c.(type) { - case *parser.SrcContext: - accounts, _, unbounded, compErr := p.VisitSource(c.Source(), pushAsset, isAll) - if compErr != nil { - return nil, compErr - } - for k, v := range accounts { - neededAccounts[k] = v - } - if !isAll { - p.PushAddress(*monAddr) - err := p.TakeFromSource(unbounded) - if err != nil { - return nil, LogicError(c, err) - } - } - case *parser.SrcAllotmentContext: - if isAll { - return nil, LogicError(c, errors.New("cannot take all balance of an allotment source")) - } - p.PushAddress(*monAddr) - p.VisitAllotment(c.SourceAllotment(), c.SourceAllotment().GetPortions()) - p.AppendInstruction(program.OP_ALLOC) - - sources := c.SourceAllotment().GetSources() - n := len(sources) - for i := 0; i < n; i++ { - accounts, _, fallback, compErr := p.VisitSource(sources[i], pushAsset, isAll) - if compErr != nil { - return nil, compErr - } - for k, v := range accounts { - neededAccounts[k] = v - } - err := p.Bump(int64(i + 1)) - if err != nil { - return nil, LogicError(c, err) - } - err = p.TakeFromSource(fallback) - if err != nil { - return nil, LogicError(c, err) - } - } - err := p.PushInteger(core.NewNumber(int64(n))) - if err != nil { - return nil, LogicError(c, err) - } - p.AppendInstruction(program.OP_FUNDING_ASSEMBLE) - } - return neededAccounts, nil -} - func (p *parseVisitor) TakeFromSource(fallback *FallbackAccount) error { if fallback == nil { p.AppendInstruction(program.OP_TAKE) @@ -75,25 +19,26 @@ func (p *parseVisitor) TakeFromSource(fallback *FallbackAccount) error { return err } p.AppendInstruction(program.OP_REPAY) - } else { - p.AppendInstruction(program.OP_TAKE_MAX) - err := p.Bump(1) - if err != nil { - return err - } - p.AppendInstruction(program.OP_REPAY) - p.PushAddress(core.Address(*fallback)) - err = p.Bump(2) - if err != nil { - return err - } - p.AppendInstruction(program.OP_TAKE_ALWAYS) - err = p.PushInteger(core.NewNumber(2)) - if err != nil { - return err - } - p.AppendInstruction(program.OP_FUNDING_ASSEMBLE) + return nil + } + + p.AppendInstruction(program.OP_TAKE_MAX) + err := p.Bump(1) + if err != nil { + return err + } + p.AppendInstruction(program.OP_REPAY) + p.PushAddress(core.Address(*fallback)) + err = p.Bump(2) + if err != nil { + return err + } + p.AppendInstruction(program.OP_TAKE_ALWAYS) + err = p.PushInteger(core.NewNumber(2)) + if err != nil { + return err } + p.AppendInstruction(program.OP_FUNDING_ASSEMBLE) return nil } diff --git a/pkg/machine/vm/machine.go b/pkg/machine/vm/machine.go index 2de15664b..d699aa599 100644 --- a/pkg/machine/vm/machine.go +++ b/pkg/machine/vm/machine.go @@ -190,7 +190,7 @@ func (m *Machine) repay(funding core.Funding) { } } -func (m *Machine) tick() (bool, byte) { +func (m *Machine) tick() (bool, byte, error) { op := m.Program.Instructions[m.P] if m.Debug { @@ -205,7 +205,7 @@ func (m *Machine) tick() (bool, byte) { bytes := m.Program.Instructions[m.P+1 : m.P+3] v, ok := m.getResource(core.Address(binary.LittleEndian.Uint16(bytes))) if !ok { - return true, EXIT_FAIL + return true, EXIT_FAIL, fmt.Errorf("%s", program.OpcodeName(op)) } m.Stack = append(m.Stack, *v) m.P += 2 @@ -220,7 +220,7 @@ func (m *Machine) tick() (bool, byte) { case program.OP_DELETE: n := m.popValue() if n.GetType() == core.TypeFunding { - return true, EXIT_FAIL_INVALID + return true, EXIT_FAIL_INVALID, fmt.Errorf("%s", program.OpcodeName(op)) } case program.OP_IADD: @@ -238,7 +238,7 @@ func (m *Machine) tick() (bool, byte) { m.printChan <- a case program.OP_FAIL: - return true, EXIT_FAIL + return true, EXIT_FAIL, nil case program.OP_ASSET: v := m.popValue() @@ -250,7 +250,7 @@ func (m *Machine) tick() (bool, byte) { case core.Funding: m.pushValue(v.Asset) default: - return true, EXIT_FAIL_INVALID + return true, EXIT_FAIL_INVALID, fmt.Errorf("%s", program.OpcodeName(op)) } case program.OP_MONETARY_NEW: @@ -265,13 +265,25 @@ func (m *Machine) tick() (bool, byte) { b := pop[core.Monetary](m) a := pop[core.Monetary](m) if a.Asset != b.Asset { - return true, EXIT_FAIL_INVALID + return true, EXIT_FAIL_INVALID, fmt.Errorf( + "tried to add two monetary with different assets: '%s' and '%s'", a.Asset, b.Asset) } m.pushValue(core.Monetary{ Asset: a.Asset, Amount: a.Amount.Add(b.Amount), }) + case program.OP_MONETARY_SUB: + b := pop[core.Monetary](m) + a := pop[core.Monetary](m) + if a.Asset != b.Asset { + return true, EXIT_FAIL_INVALID, fmt.Errorf("%s", program.OpcodeName(op)) + } + m.pushValue(core.Monetary{ + Asset: a.Asset, + Amount: a.Amount.Sub(b.Amount), + }) + case program.OP_MAKE_ALLOTMENT: n := pop[core.Number](m) portions := make([]core.Portion, n.Uint64()) @@ -281,7 +293,7 @@ func (m *Machine) tick() (bool, byte) { } allotment, err := core.NewAllotment(portions) if err != nil { - return true, EXIT_FAIL_INVALID + return true, EXIT_FAIL_INVALID, fmt.Errorf("%s", program.OpcodeName(op)) } m.pushValue(*allotment) @@ -290,7 +302,7 @@ func (m *Machine) tick() (bool, byte) { account := pop[core.AccountAddress](m) funding, err := m.withdrawAll(account, overdraft.Asset, overdraft.Amount) if err != nil { - return true, EXIT_FAIL_INVALID + return true, EXIT_FAIL_INVALID, fmt.Errorf("%s", program.OpcodeName(op)) } m.pushValue(*funding) @@ -299,7 +311,7 @@ func (m *Machine) tick() (bool, byte) { account := pop[core.AccountAddress](m) funding, err := m.withdrawAlways(account, mon) if err != nil { - return true, EXIT_FAIL_INVALID + return true, EXIT_FAIL_INVALID, fmt.Errorf("%s", program.OpcodeName(op)) } m.pushValue(*funding) @@ -307,31 +319,36 @@ func (m *Machine) tick() (bool, byte) { mon := pop[core.Monetary](m) funding := pop[core.Funding](m) if funding.Asset != mon.Asset { - return true, EXIT_FAIL_INVALID + return true, EXIT_FAIL_INVALID, fmt.Errorf("%s", program.OpcodeName(op)) } result, remainder, err := funding.Take(mon.Amount) if err != nil { - return true, EXIT_FAIL_INSUFFICIENT_FUNDS + return true, EXIT_FAIL_INSUFFICIENT_FUNDS, nil } m.pushValue(remainder) m.pushValue(result) case program.OP_TAKE_MAX: mon := pop[core.Monetary](m) + if mon.Amount.Ltz() { + return true, EXIT_FAIL_INVALID, fmt.Errorf( + "cannot send a monetary with a negative amount: [%s %s]", + string(mon.Asset), mon.Amount) + } funding := pop[core.Funding](m) if funding.Asset != mon.Asset { - return true, EXIT_FAIL_INVALID + return true, EXIT_FAIL_INVALID, fmt.Errorf("%s", program.OpcodeName(op)) } missing := core.NewMonetaryInt(0) total := funding.Total() if mon.Amount.Gt(total) { missing = mon.Amount.Sub(total) } - result, remainder := funding.TakeMax(mon.Amount) m.pushValue(core.Monetary{ Asset: mon.Asset, Amount: missing, }) + result, remainder := funding.TakeMax(mon.Amount) m.pushValue(remainder) m.pushValue(result) @@ -339,7 +356,7 @@ func (m *Machine) tick() (bool, byte) { num := pop[core.Number](m) n := int(num.Uint64()) if n == 0 { - return true, EXIT_FAIL_INVALID + return true, EXIT_FAIL_INVALID, fmt.Errorf("%s", program.OpcodeName(op)) } first := pop[core.Funding](m) result := core.Funding{ @@ -350,14 +367,14 @@ func (m *Machine) tick() (bool, byte) { for i := 1; i < n; i++ { f := pop[core.Funding](m) if f.Asset != result.Asset { - return true, EXIT_FAIL_INVALID + return true, EXIT_FAIL_INVALID, fmt.Errorf("%s", program.OpcodeName(op)) } fundings_rev[i] = f } for i := 0; i < n; i++ { res, err := result.Concat(fundings_rev[n-1-i]) if err != nil { - return true, EXIT_FAIL_INVALID + return true, EXIT_FAIL_INVALID, fmt.Errorf("%s", program.OpcodeName(op)) } result = res } @@ -425,16 +442,16 @@ func (m *Machine) tick() (bool, byte) { m.AccountsMeta[a][string(k)] = v default: - return true, EXIT_FAIL_INVALID + return true, EXIT_FAIL_INVALID, fmt.Errorf("%s", program.OpcodeName(op)) } m.P += 1 if int(m.P) >= len(m.Program.Instructions) { - return true, EXIT_OK + return true, EXIT_OK, nil } - return false, 0 + return false, 0, nil } func (m *Machine) Execute() (byte, error) { @@ -448,12 +465,12 @@ func (m *Machine) Execute() (byte, error) { } for { - finished, exitCode := m.tick() + finished, exitCode, err := m.tick() if finished { if exitCode == EXIT_OK && len(m.Stack) != 0 { - return EXIT_FAIL_INVALID, nil + return EXIT_FAIL_INVALID, err } else { - return exitCode, nil + return exitCode, err } } } diff --git a/pkg/machine/vm/machine_test.go b/pkg/machine/vm/machine_test.go index 2f1e2a9cd..456ba81fc 100644 --- a/pkg/machine/vm/machine_test.go +++ b/pkg/machine/vm/machine_test.go @@ -1844,3 +1844,88 @@ func TestVariableAsset(t *testing.T) { } test(t, tc) } + +func TestSendWithArithmetic(t *testing.T) { + t.Run("nominal", func(t *testing.T) { + tc := NewTestCase() + script := ` + vars { + asset $ass + monetary $mon + } + send [EUR 1] + $mon + [$ass 3] - [EUR 4] ( + source = @a + destination = @b + )` + tc.compile(t, script) + tc.setBalance("a", "EUR", 10) + tc.vars = map[string]core.Value{ + "ass": core.Asset("EUR"), + "mon": core.Monetary{ + Asset: "EUR", + Amount: core.NewMonetaryInt(2), + }, + } + tc.expected = CaseResult{ + Printed: []core.Value{}, + Postings: []Posting{ + { + Asset: "EUR", + Amount: core.NewMonetaryInt(2), + Source: "a", + Destination: "b", + }, + }, + ExitCode: EXIT_OK, + } + test(t, tc) + }) + + t.Run("error different assets", func(t *testing.T) { + tc := NewTestCase() + tc.compile(t, ` + send [USD 2] + [EUR 1] ( + source = @world + destination = @alice + )`) + tc.expected = CaseResult{ + Printed: []core.Value{}, + Postings: []Posting{}, + ExitCode: EXIT_FAIL_INVALID, + Error: "tried to add two monetary with different assets: 'USD' and 'EUR'", + } + test(t, tc) + }) + + t.Run("error negative amount", func(t *testing.T) { + tc := NewTestCase() + tc.compile(t, ` + send [USD 2] - [USD 3] ( + source = @world + destination = @alice + )`) + tc.expected = CaseResult{ + Printed: []core.Value{}, + Postings: []Posting{}, + ExitCode: EXIT_FAIL_INVALID, + Error: "cannot send a monetary with a negative amount: [USD -1]", + } + test(t, tc) + }) + + t.Run("error insufficient funds", func(t *testing.T) { + tc := NewTestCase() + tc.compile(t, ` + send [USD 3] - [USD 1] ( + source = @bob + destination = @alice + )`) + tc.setBalance("bob", "USD", 1) + tc.expected = CaseResult{ + Printed: []core.Value{}, + Postings: []Posting{}, + ExitCode: EXIT_FAIL_INSUFFICIENT_FUNDS, + } + test(t, tc) + }) +} diff --git a/pkg/machine/vm/program/instructions.go b/pkg/machine/vm/program/instructions.go index ac9ee4993..97c897024 100644 --- a/pkg/machine/vm/program/instructions.go +++ b/pkg/machine/vm/program/instructions.go @@ -4,13 +4,14 @@ const ( OP_APUSH = byte(iota + 1) OP_BUMP // *N => *N OP_DELETE // - OP_IADD // => - OP_ISUB // => + OP_IADD // + => + OP_ISUB // - => OP_PRINT // OP_FAIL // OP_ASSET // => OP_MONETARY_NEW // => - OP_MONETARY_ADD // => // panics if not same asset + OP_MONETARY_ADD // + => // panics if not same asset + OP_MONETARY_SUB // - => // panics if not same asset OP_MAKE_ALLOTMENT // *N => OP_TAKE_ALL // => OP_TAKE_ALWAYS // => // takes amount from account unconditionally @@ -48,6 +49,8 @@ func OpcodeName(op byte) string { return "OP_MONETARY_NEW" case OP_MONETARY_ADD: return "OP_MONETARY_ADD" + case OP_MONETARY_SUB: + return "OP_MONETARY_SUB" case OP_MAKE_ALLOTMENT: return "OP_MAKE_ALLOTMENT" case OP_TAKE_ALL: