From 8d399c9672829fdebfc1a3fb39da268baa193960 Mon Sep 17 00:00:00 2001 From: Azorlogh Date: Mon, 13 Oct 2025 17:04:20 +0200 Subject: [PATCH 1/2] simplify fundsQueue & rename interpreter functions --- internal/interpreter/batch_balances_query.go | 6 +- internal/interpreter/funds_queue.go | 135 +++++++++++++ ...unds_stack_test.go => funds_queue_test.go} | 71 +++---- internal/interpreter/funds_stack.go | 188 ------------------ internal/interpreter/interpreter.go | 116 +++++------ 5 files changed, 233 insertions(+), 283 deletions(-) create mode 100644 internal/interpreter/funds_queue.go rename internal/interpreter/{funds_stack_test.go => funds_queue_test.go} (72%) delete mode 100644 internal/interpreter/funds_stack.go diff --git a/internal/interpreter/batch_balances_query.go b/internal/interpreter/batch_balances_query.go index cedd25ec..bff6e58a 100644 --- a/internal/interpreter/batch_balances_query.go +++ b/internal/interpreter/batch_balances_query.go @@ -39,7 +39,7 @@ func (st *programState) findBalancesQueriesInStatement(statement parser.Statemen if err != nil { return err } - st.CurrentAsset = *asset + st.fundsQueue.asset = *asset // traverse source return st.findBalancesQueries(statement.Source) @@ -95,7 +95,7 @@ func (st *programState) findBalancesQueries(source parser.Source) InterpreterErr return err } - st.batchQuery(*account, st.CurrentAsset, color) + st.batchQuery(*account, st.fundsQueue.asset, color) return nil case *parser.SourceOverdraft: @@ -113,7 +113,7 @@ func (st *programState) findBalancesQueries(source parser.Source) InterpreterErr return err } - st.batchQuery(*account, st.CurrentAsset, color) + st.batchQuery(*account, st.fundsQueue.asset, color) return nil case *parser.SourceInorder: diff --git a/internal/interpreter/funds_queue.go b/internal/interpreter/funds_queue.go new file mode 100644 index 00000000..e5cb9e35 --- /dev/null +++ b/internal/interpreter/funds_queue.go @@ -0,0 +1,135 @@ +package interpreter + +import ( + "fmt" + "math/big" + "slices" +) + +type Sender struct { + Name string + Amount *big.Int + Color string +} + +type fundsQueue struct { + asset string + senders []Sender +} + +// Create a fundsQueue from a slice of senders. +func newFundsQueue(senders []Sender) fundsQueue { + queue := fundsQueue{ + senders: []Sender{}, + } + queue.Push(senders...) + return queue +} + +// Push senders to this fundsQueue +func (s *fundsQueue) Push(senders ...Sender) { + for _, sender := range senders { + s.PushOne(sender) + } +} + +// Push a single sender to this queue +func (s *fundsQueue) PushOne(sender Sender) { + if sender.Amount.Cmp(big.NewInt(0)) == 0 { + return + } + if len(s.senders) == 0 { + s.senders = []Sender{sender} + return + } + last := s.senders[len(s.senders)-1] + if last.Name == sender.Name && last.Color == sender.Color { + last.Amount.Add(last.Amount, sender.Amount) + } else { + s.senders = append(s.senders, sender) + } +} + +// Pull everything from this queue +func (s *fundsQueue) PullAll() []Sender { + senders := s.senders + s.senders = []Sender{} + return senders +} + +// Pull at most maxAmount from this queue, with any color +func (s *fundsQueue) PullAnything(maxAmount *big.Int) []Sender { + return s.Pull(maxAmount, nil) +} + +func (s *fundsQueue) PullColored(maxAmount *big.Int, color string) []Sender { + return s.Pull(maxAmount, &color) +} +func (s *fundsQueue) PullUncolored(maxAmount *big.Int) []Sender { + return s.PullColored(maxAmount, "") +} + +// Pull at most maxAmount from this queue, with the given color +func (s *fundsQueue) Pull(maxAmount *big.Int, color *string) []Sender { + // clone so that we can manipulate this arg + maxAmount = new(big.Int).Set(maxAmount) + + // TODO preallocate for perfs + out := newFundsQueue([]Sender{}) + offset := 0 + + for maxAmount.Cmp(big.NewInt(0)) != 0 && len(s.senders) > offset { + + frontSender := s.senders[offset] + + if color != nil && frontSender.Color != *color { + offset += 1 + continue + } + + switch frontSender.Amount.Cmp(maxAmount) { + case -1: // not enough + maxAmount.Sub(maxAmount, frontSender.Amount) + out.Push(frontSender) + s.senders = slices.Delete(s.senders, offset, offset+1) + case 1: // more than enough + out.Push(Sender{ + Name: frontSender.Name, + Amount: maxAmount, + Color: frontSender.Color, + }) + s.senders[offset].Amount.Sub(s.senders[offset].Amount, maxAmount) + return out.senders + case 0: // exactly enough + out.Push(s.senders[offset]) + s.senders = slices.Delete(s.senders, offset, offset+1) + return out.senders + } + } + + return out.senders +} + +// Clone the queue so that you can safely mutate one without mutating the other +func (s fundsQueue) Clone() fundsQueue { + return fundsQueue{ + senders: slices.Clone(s.senders), + asset: s.asset, + } +} + +func (s fundsQueue) String() string { + out := ">" + for i, sender := range s.senders { + if sender.Color == "" { + out += fmt.Sprintf("%v from %v", sender.Amount, sender.Name) + } else { + out += fmt.Sprintf("%v from %v\\%v", sender.Amount, sender.Name, sender.Color) + } + if i != len(s.senders)-1 { + out += ", " + } + } + out += ">" + return out +} diff --git a/internal/interpreter/funds_stack_test.go b/internal/interpreter/funds_queue_test.go similarity index 72% rename from internal/interpreter/funds_stack_test.go rename to internal/interpreter/funds_queue_test.go index a7a5d6af..9725d5d8 100644 --- a/internal/interpreter/funds_stack_test.go +++ b/internal/interpreter/funds_queue_test.go @@ -8,11 +8,11 @@ import ( ) func TestEnoughBalance(t *testing.T) { - stack := newFundsStack([]Sender{ + queue := newFundsQueue([]Sender{ {Name: "s1", Amount: big.NewInt(100)}, }) - out := stack.PullAnything(big.NewInt(2)) + out := queue.PullAnything(big.NewInt(2)) require.Equal(t, []Sender{ {Name: "s1", Amount: big.NewInt(2)}, }, out) @@ -20,10 +20,10 @@ func TestEnoughBalance(t *testing.T) { } func TestPush(t *testing.T) { - stack := newFundsStack(nil) - stack.Push(Sender{Name: "acc", Amount: big.NewInt(100)}) + queue := newFundsQueue(nil) + queue.Push(Sender{Name: "acc", Amount: big.NewInt(100)}) - out := stack.PullUncolored(big.NewInt(20)) + out := queue.PullUncolored(big.NewInt(20)) require.Equal(t, []Sender{ {Name: "acc", Amount: big.NewInt(20)}, }, out) @@ -31,107 +31,108 @@ func TestPush(t *testing.T) { } func TestSimple(t *testing.T) { - stack := newFundsStack([]Sender{ + queue := newFundsQueue([]Sender{ {Name: "s1", Amount: big.NewInt(2)}, {Name: "s2", Amount: big.NewInt(10)}, }) - out := stack.PullAnything(big.NewInt(5)) + out := queue.PullAnything(big.NewInt(5)) require.Equal(t, []Sender{ {Name: "s1", Amount: big.NewInt(2)}, {Name: "s2", Amount: big.NewInt(3)}, }, out) - out = stack.PullAnything(big.NewInt(7)) + out = queue.PullAnything(big.NewInt(7)) require.Equal(t, []Sender{ {Name: "s2", Amount: big.NewInt(7)}, }, out) } func TestPullZero(t *testing.T) { - stack := newFundsStack([]Sender{ + queue := newFundsQueue([]Sender{ {Name: "s1", Amount: big.NewInt(2)}, {Name: "s2", Amount: big.NewInt(10)}, }) - out := stack.PullAnything(big.NewInt(0)) - require.Equal(t, []Sender(nil), out) + out := queue.PullAnything(big.NewInt(0)) + require.Equal(t, []Sender{}, out) } func TestCompactFunds(t *testing.T) { - stack := newFundsStack([]Sender{ + queue := newFundsQueue([]Sender{ {Name: "s1", Amount: big.NewInt(2)}, {Name: "s1", Amount: big.NewInt(10)}, }) - out := stack.PullAnything(big.NewInt(5)) + out := queue.PullAnything(big.NewInt(5)) require.Equal(t, []Sender{ {Name: "s1", Amount: big.NewInt(5)}, }, out) } func TestCompactFunds3Times(t *testing.T) { - stack := newFundsStack([]Sender{ + queue := newFundsQueue([]Sender{ {Name: "s1", Amount: big.NewInt(2)}, {Name: "s1", Amount: big.NewInt(3)}, {Name: "s1", Amount: big.NewInt(1)}, }) - out := stack.PullAnything(big.NewInt(6)) + out := queue.PullAnything(big.NewInt(6)) require.Equal(t, []Sender{ {Name: "s1", Amount: big.NewInt(6)}, }, out) } func TestCompactFundsWithEmptySender(t *testing.T) { - stack := newFundsStack([]Sender{ + queue := newFundsQueue([]Sender{ {Name: "s1", Amount: big.NewInt(2)}, {Name: "s2", Amount: big.NewInt(0)}, {Name: "s1", Amount: big.NewInt(10)}, }) - out := stack.PullAnything(big.NewInt(5)) + out := queue.PullAnything(big.NewInt(5)) + require.Equal(t, []Sender{ {Name: "s1", Amount: big.NewInt(5)}, }, out) } func TestMissingFunds(t *testing.T) { - stack := newFundsStack([]Sender{ + queue := newFundsQueue([]Sender{ {Name: "s1", Amount: big.NewInt(2)}, }) - out := stack.PullAnything(big.NewInt(300)) + out := queue.PullAnything(big.NewInt(300)) require.Equal(t, []Sender{ {Name: "s1", Amount: big.NewInt(2)}, }, out) } func TestNoZeroLeftovers(t *testing.T) { - stack := newFundsStack([]Sender{ + queue := newFundsQueue([]Sender{ {Name: "s1", Amount: big.NewInt(10)}, {Name: "s2", Amount: big.NewInt(15)}, }) - stack.PullAnything(big.NewInt(10)) + queue.PullAnything(big.NewInt(10)) - out := stack.PullAnything(big.NewInt(15)) + out := queue.PullAnything(big.NewInt(15)) require.Equal(t, []Sender{ {Name: "s2", Amount: big.NewInt(15)}, }, out) } func TestReconcileColoredManyDestPerSender(t *testing.T) { - stack := newFundsStack([]Sender{ + queue := newFundsQueue([]Sender{ {"src", big.NewInt(10), "X"}, }) - out := stack.PullColored(big.NewInt(5), "X") + out := queue.PullColored(big.NewInt(5), "X") require.Equal(t, []Sender{ {Name: "src", Amount: big.NewInt(5), Color: "X"}, }, out) - out = stack.PullColored(big.NewInt(5), "X") + out = queue.PullColored(big.NewInt(5), "X") require.Equal(t, []Sender{ {Name: "src", Amount: big.NewInt(5), Color: "X"}, }, out) @@ -139,7 +140,7 @@ func TestReconcileColoredManyDestPerSender(t *testing.T) { } func TestPullColored(t *testing.T) { - stack := newFundsStack([]Sender{ + queue := newFundsQueue([]Sender{ {Name: "s1", Amount: big.NewInt(5)}, {Name: "s2", Amount: big.NewInt(1), Color: "red"}, {Name: "s3", Amount: big.NewInt(10)}, @@ -147,7 +148,7 @@ func TestPullColored(t *testing.T) { {Name: "s5", Amount: big.NewInt(5)}, }) - out := stack.PullColored(big.NewInt(2), "red") + out := queue.PullColored(big.NewInt(2), "red") require.Equal(t, []Sender{ {Name: "s2", Amount: big.NewInt(1), Color: "red"}, {Name: "s4", Amount: big.NewInt(1), Color: "red"}, @@ -158,16 +159,16 @@ func TestPullColored(t *testing.T) { {Name: "s3", Amount: big.NewInt(10)}, {Name: "s4", Amount: big.NewInt(1), Color: "red"}, {Name: "s5", Amount: big.NewInt(5)}, - }, stack.PullAll()) + }, queue.PullAll()) } func TestPullColoredComplex(t *testing.T) { - stack := newFundsStack([]Sender{ + queue := newFundsQueue([]Sender{ {"s1", big.NewInt(1), "c1"}, {"s2", big.NewInt(1), "c2"}, }) - out := stack.PullColored(big.NewInt(1), "c2") + out := queue.PullColored(big.NewInt(1), "c2") require.Equal(t, []Sender{ {Name: "s2", Amount: big.NewInt(1), Color: "c2"}, }, out) @@ -175,7 +176,7 @@ func TestPullColoredComplex(t *testing.T) { func TestClone(t *testing.T) { - fs := newFundsStack([]Sender{ + fs := newFundsQueue([]Sender{ {"s1", big.NewInt(10), ""}, }) @@ -192,19 +193,19 @@ func TestClone(t *testing.T) { func TestCompactFundsAndPush(t *testing.T) { noCol := "" - stack := newFundsStack([]Sender{ + queue := newFundsQueue([]Sender{ {Name: "s1", Amount: big.NewInt(2)}, {Name: "s1", Amount: big.NewInt(10)}, }) - stack.Pull(big.NewInt(1), &noCol) + queue.Pull(big.NewInt(1), &noCol) - stack.Push(Sender{ + queue.Push(Sender{ Name: "pushed", Amount: big.NewInt(42), }) - out := stack.PullAll() + out := queue.PullAll() require.Equal(t, []Sender{ {Name: "s1", Amount: big.NewInt(11)}, {Name: "pushed", Amount: big.NewInt(42)}, diff --git a/internal/interpreter/funds_stack.go b/internal/interpreter/funds_stack.go deleted file mode 100644 index ca82877c..00000000 --- a/internal/interpreter/funds_stack.go +++ /dev/null @@ -1,188 +0,0 @@ -package interpreter - -import ( - "math/big" -) - -type Sender struct { - Name string - Amount *big.Int - Color string -} - -type stack[T any] struct { - Head T - Tail *stack[T] - - // Instead of keeping a single ref of the lastCell and updating the invariant on every push/pop operation, - // we keep a cache of the last cell on every cell. - // This makes code much easier and we don't risk breaking the invariant and producing wrong results and other subtle issues - // - // While, unlike keeping a single reference (like golang's queue `container/list` package does), this is not always O(1), - // the amortized time should still be O(1) (the number of steps of traversal while searching the last elem is not higher than the number of .Push() calls) - lastCell *stack[T] -} - -func (s *stack[T]) getLastCell() *stack[T] { - // check if this is the last cell without reading cache first - if s.Tail == nil { - return s - } - - // if not, check if cache is present - if s.lastCell != nil { - // even if it is, it may be a stale value (as more values could have been pushed), so we check the value recursively - lastCell := s.lastCell.getLastCell() - // we do path compression so that next time we get the path immediately - s.lastCell = lastCell - return lastCell - } - - // if no last value is cached, we traverse recursively to find it - s.lastCell = s.Tail.getLastCell() - return s.lastCell -} - -func fromSlice[T any](slice []T) *stack[T] { - var ret *stack[T] - // TODO use https://pkg.go.dev/slices#Backward in golang 1.23 - for i := len(slice) - 1; i >= 0; i-- { - ret = &stack[T]{ - Head: slice[i], - Tail: ret, - } - } - return ret -} - -type fundsStack struct { - senders *stack[Sender] -} - -func newFundsStack(senders []Sender) fundsStack { - return fundsStack{ - senders: fromSlice(senders), - } -} - -func (s *fundsStack) compactTop() { - for s.senders != nil && s.senders.Tail != nil { - - first := s.senders.Head - second := s.senders.Tail.Head - - if second.Amount.Cmp(big.NewInt(0)) == 0 { - s.senders = &stack[Sender]{Head: first, Tail: s.senders.Tail.Tail} - continue - } - - if first.Name != second.Name || first.Color != second.Color { - return - } - - s.senders = &stack[Sender]{ - Head: Sender{ - Name: first.Name, - Color: first.Color, - Amount: new(big.Int).Add(first.Amount, second.Amount), - }, - Tail: s.senders.Tail.Tail, - } - } -} - -func (s *fundsStack) PullAll() []Sender { - var senders []Sender - for s.senders != nil { - senders = append(senders, s.senders.Head) - s.senders = s.senders.Tail - } - return senders -} - -func (s *fundsStack) Push(senders ...Sender) { - newTail := fromSlice(senders) - if s.senders == nil { - s.senders = newTail - } else { - cell := s.senders.getLastCell() - cell.Tail = newTail - } -} - -func (s *fundsStack) PullAnything(requiredAmount *big.Int) []Sender { - return s.Pull(requiredAmount, nil) -} - -func (s *fundsStack) PullColored(requiredAmount *big.Int, color string) []Sender { - return s.Pull(requiredAmount, &color) -} -func (s *fundsStack) PullUncolored(requiredAmount *big.Int) []Sender { - return s.PullColored(requiredAmount, "") -} - -func (s *fundsStack) Pull(requiredAmount *big.Int, color *string) []Sender { - // clone so that we can manipulate this arg - requiredAmount = new(big.Int).Set(requiredAmount) - - // TODO preallocate for perfs - var out []Sender - - for requiredAmount.Cmp(big.NewInt(0)) != 0 && s.senders != nil { - s.compactTop() - - available := s.senders.Head - s.senders = s.senders.Tail - - if color != nil && available.Color != *color { - out1 := s.Pull(requiredAmount, color) - s.senders = &stack[Sender]{ - Head: available, - Tail: s.senders, - } - out = append(out, out1...) - break - } - - switch available.Amount.Cmp(requiredAmount) { - case -1: // not enough: - out = append(out, available) - requiredAmount.Sub(requiredAmount, available.Amount) - - case 1: // more than enough - s.senders = &stack[Sender]{ - Head: Sender{ - Name: available.Name, - Color: available.Color, - Amount: new(big.Int).Sub(available.Amount, requiredAmount), - }, - Tail: s.senders, - } - fallthrough - - case 0: // exactly the same - out = append(out, Sender{ - Name: available.Name, - Color: available.Color, - Amount: new(big.Int).Set(requiredAmount), - }) - return out - } - - } - - return out -} - -// Clone the stack so that you can safely mutate one without mutating the other -func (s fundsStack) Clone() fundsStack { - fs := newFundsStack(nil) - - senders := s.senders - for senders != nil { - fs.Push(senders.Head) - senders = senders.Tail - } - - return fs -} diff --git a/internal/interpreter/interpreter.go b/internal/interpreter/interpreter.go index 570da1f4..7a9a17c6 100644 --- a/internal/interpreter/interpreter.go +++ b/internal/interpreter/interpreter.go @@ -240,7 +240,7 @@ func RunProgram( SetAccountsMeta: AccountsMetadata{}, Store: store, Postings: make([]Posting, 0), - fundsStack: newFundsStack(nil), + fundsQueue: newFundsQueue([]Sender{}), CurrentBalanceQuery: BalanceQuery{}, ctx: ctx, @@ -289,15 +289,13 @@ type programState struct { varOriginPosition bool - // Asset of the send statement currently being executed. - // + // fundsQueue of the send statement currently being executed. // its value is undefined outside of send statements execution - CurrentAsset string + fundsQueue fundsQueue ParsedVars map[string]Value TxMeta map[string]Value Postings []Posting - fundsStack fundsStack Store Store @@ -316,36 +314,36 @@ func (st *programState) pushSender(name string, monetary *big.Int, color string) return } - balance := st.CachedBalances.fetchBalance(name, st.CurrentAsset, color) + balance := st.CachedBalances.fetchBalance(name, st.fundsQueue.asset, color) balance.Sub(balance, monetary) - st.fundsStack.Push(Sender{Name: name, Amount: monetary, Color: color}) + st.fundsQueue.Push(Sender{Name: name, Amount: monetary, Color: color}) } -func (st *programState) pushReceiver(name string, monetary *big.Int) { +func (st *programState) sendToAccount(name string, monetary *big.Int) { if monetary.Cmp(big.NewInt(0)) == 0 { return } - senders := st.fundsStack.PullAnything(monetary) + senders := st.fundsQueue.PullAnything(monetary) for _, sender := range senders { postings := Posting{ Source: sender.Name, Destination: name, - Asset: coloredAsset(st.CurrentAsset, &sender.Color), + Asset: coloredAsset(st.fundsQueue.asset, &sender.Color), Amount: sender.Amount, } if name == KEPT_ADDR { // If funds are kept, give them back to senders - srcBalance := st.CachedBalances.fetchBalance(postings.Source, st.CurrentAsset, sender.Color) + srcBalance := st.CachedBalances.fetchBalance(postings.Source, st.fundsQueue.asset, sender.Color) srcBalance.Add(srcBalance, postings.Amount) continue } - destBalance := st.CachedBalances.fetchBalance(postings.Destination, st.CurrentAsset, sender.Color) + destBalance := st.CachedBalances.fetchBalance(postings.Destination, st.fundsQueue.asset, sender.Color) destBalance.Add(destBalance, postings.Amount) st.Postings = append(st.Postings, postings) @@ -423,32 +421,32 @@ func (st *programState) runSendStatement(statement parser.SendStatement) Interpr if err != nil { return err } - st.CurrentAsset = *asset - sentAmt, err := st.sendAll(statement.Source) + st.fundsQueue.asset = *asset + sentAmt, err := st.takeAll(statement.Source) if err != nil { return err } - return st.receiveFrom(statement.Destination, sentAmt) + return st.sendTo(statement.Destination, sentAmt) case *parser.SentValueLiteral: monetary, err := evaluateExprAs(st, sentValue.Monetary, expectMonetary) if err != nil { return err } - st.CurrentAsset = string(monetary.Asset) + st.fundsQueue.asset = string(monetary.Asset) monetaryAmt := (*big.Int)(&monetary.Amount) if monetaryAmt.Cmp(big.NewInt(0)) == -1 { return NegativeAmountErr{Amount: monetary.Amount} } - err = st.trySendingExact(statement.Source, monetaryAmt) + err = st.tryTakingExact(statement.Source, monetaryAmt) if err != nil { return err } amt := big.Int(monetary.Amount) - return st.receiveFrom(statement.Destination, &amt) + return st.sendTo(statement.Destination, &amt) default: utils.NonExhaustiveMatchPanic[any](sentValue) return nil @@ -457,7 +455,7 @@ func (st *programState) runSendStatement(statement parser.SendStatement) Interpr } // PRE: overdraft >= 0 -func (s *programState) sendAllToAccount(accountLiteral parser.ValueExpr, overdraft *big.Int, colorExpr parser.ValueExpr) (*big.Int, InterpreterError) { +func (s *programState) takeAllFromAccount(accountLiteral parser.ValueExpr, overdraft *big.Int, colorExpr parser.ValueExpr) (*big.Int, InterpreterError) { if colorExpr != nil { err := s.checkFeatureFlag(flags.ExperimentalAssetColors) if err != nil { @@ -481,36 +479,37 @@ func (s *programState) sendAllToAccount(accountLiteral parser.ValueExpr, overdra return nil, err } - balance := s.CachedBalances.fetchBalance(*account, s.CurrentAsset, *color) + balance := s.CachedBalances.fetchBalance(*account, s.fundsQueue.asset, *color) // we sent balance+overdraft sentAmt := CalculateMaxSafeWithdraw(balance, overdraft) s.pushSender(*account, sentAmt, *color) + return sentAmt, nil } // Send as much as possible (and return the sent amt) -func (s *programState) sendAll(source parser.Source) (*big.Int, InterpreterError) { +func (s *programState) takeAll(source parser.Source) (*big.Int, InterpreterError) { switch source := source.(type) { case *parser.SourceAccount: - return s.sendAllToAccount(source.ValueExpr, big.NewInt(0), source.Color) + return s.takeAllFromAccount(source.ValueExpr, big.NewInt(0), source.Color) case *parser.SourceOverdraft: var cap *big.Int if source.Bounded != nil { - bounded, err := evaluateExprAs(s, *source.Bounded, expectMonetaryOfAsset(s.CurrentAsset)) + bounded, err := evaluateExprAs(s, *source.Bounded, expectMonetaryOfAsset(s.fundsQueue.asset)) if err != nil { return nil, err } cap = utils.NonNeg(bounded) } - return s.sendAllToAccount(source.Address, cap, source.Color) + return s.takeAllFromAccount(source.Address, cap, source.Color) case *parser.SourceInorder: totalSent := big.NewInt(0) for _, subSource := range source.Sources { - sent, err := s.sendAll(subSource) + sent, err := s.takeAll(subSource) if err != nil { return nil, err } @@ -526,15 +525,15 @@ func (s *programState) sendAll(source parser.Source) (*big.Int, InterpreterError // we can safely access the first one because empty oneof is parsing err first := source.Sources[0] - return s.sendAll(first) + return s.takeAll(first) case *parser.SourceCapped: - monetary, err := evaluateExprAs(s, source.Cap, expectMonetaryOfAsset(s.CurrentAsset)) + monetary, err := evaluateExprAs(s, source.Cap, expectMonetaryOfAsset(s.fundsQueue.asset)) if err != nil { return nil, err } // We switch to the default sending evaluation for this subsource - return s.trySendingUpTo(source.From, utils.NonNeg(monetary)) + return s.tryTakingUpTo(source.From, utils.NonNeg(monetary)) case *parser.SourceAllotment: return nil, InvalidAllotmentInSendAll{} @@ -546,14 +545,14 @@ func (s *programState) sendAll(source parser.Source) (*big.Int, InterpreterError } // Fails if it doesn't manage to send exactly "amount" -func (s *programState) trySendingExact(source parser.Source, amount *big.Int) InterpreterError { - sentAmt, err := s.trySendingUpTo(source, amount) +func (s *programState) tryTakingExact(source parser.Source, amount *big.Int) InterpreterError { + sentAmt, err := s.tryTakingUpTo(source, amount) if err != nil { return err } if sentAmt.Cmp(amount) != 0 { return MissingFundsErr{ - Asset: s.CurrentAsset, + Asset: s.fundsQueue.asset, Needed: *amount, Available: *sentAmt, Range: source.GetRange(), @@ -565,7 +564,7 @@ func (s *programState) trySendingExact(source parser.Source, amount *big.Int) In var colorRe = regexp.MustCompile("^[A-Z]*$") // PRE: overdraft >= 0 -func (s *programState) trySendingToAccount(accountLiteral parser.ValueExpr, amount *big.Int, overdraft *big.Int, colorExpr parser.ValueExpr) (*big.Int, InterpreterError) { +func (s *programState) tryTakingFromAccount(accountLiteral parser.ValueExpr, amount *big.Int, overdraft *big.Int, colorExpr parser.ValueExpr) (*big.Int, InterpreterError) { if colorExpr != nil { err := s.checkFeatureFlag(flags.ExperimentalAssetColors) if err != nil { @@ -591,47 +590,50 @@ func (s *programState) trySendingToAccount(accountLiteral parser.ValueExpr, amou // unbounded overdraft: we send the required amount actuallySentAmt = new(big.Int).Set(amount) } else { - balance := s.CachedBalances.fetchBalance(*account, s.CurrentAsset, *color) + balance := s.CachedBalances.fetchBalance(*account, s.fundsQueue.asset, *color) // that's the amount we are allowed to send (balance + overdraft) actuallySentAmt = CalculateSafeWithdraw(balance, overdraft, amount) } + s.pushSender(*account, actuallySentAmt, *color) + return actuallySentAmt, nil } +// Saves the state and returns a function to restore it func (s *programState) cloneState() func() { - fsBackup := s.fundsStack.Clone() balancesBackup := s.CachedBalances.DeepClone() + fundsBackup := s.fundsQueue.Clone() return func() { - s.fundsStack = fsBackup s.CachedBalances = balancesBackup + s.fundsQueue = fundsBackup } } // Tries sending "amount" and returns the actually sent amt. // Doesn't fail (unless nested sources fail) -func (s *programState) trySendingUpTo(source parser.Source, amount *big.Int) (*big.Int, InterpreterError) { +func (s *programState) tryTakingUpTo(source parser.Source, amount *big.Int) (*big.Int, InterpreterError) { switch source := source.(type) { case *parser.SourceAccount: - return s.trySendingToAccount(source.ValueExpr, amount, big.NewInt(0), source.Color) + return s.tryTakingFromAccount(source.ValueExpr, amount, big.NewInt(0), source.Color) case *parser.SourceOverdraft: var cap *big.Int if source.Bounded != nil { - upTo, err := evaluateExprAs(s, *source.Bounded, expectMonetaryOfAsset(s.CurrentAsset)) + upTo, err := evaluateExprAs(s, *source.Bounded, expectMonetaryOfAsset(s.fundsQueue.asset)) if err != nil { return nil, err } cap = utils.NonNeg(upTo) } - return s.trySendingToAccount(source.Address, amount, cap, source.Color) + return s.tryTakingFromAccount(source.Address, amount, cap, source.Color) case *parser.SourceInorder: totalLeft := new(big.Int).Set(amount) for _, source := range source.Sources { - sentAmt, err := s.trySendingUpTo(source, totalLeft) + sentAmt, err := s.tryTakingUpTo(source, totalLeft) if err != nil { return nil, err } @@ -649,10 +651,10 @@ func (s *programState) trySendingUpTo(source parser.Source, amount *big.Int) (*b leadingSources := source.Sources[0 : len(source.Sources)-1] for _, source := range leadingSources { - // do not move this line below (as .trySendingUpTo() will mutate the fundsStack) + // do not move this line below (as .trySendingUpTo() will mutate the cached balances) undo := s.cloneState() - sentAmt, err := s.trySendingUpTo(source, amount) + sentAmt, err := s.tryTakingUpTo(source, amount) if err != nil { return nil, err } @@ -666,7 +668,7 @@ func (s *programState) trySendingUpTo(source parser.Source, amount *big.Int) (*b undo() } - return s.trySendingUpTo(source.Sources[len(source.Sources)-1], amount) + return s.tryTakingUpTo(source.Sources[len(source.Sources)-1], amount) case *parser.SourceAllotment: var items []parser.AllotmentValue @@ -678,7 +680,7 @@ func (s *programState) trySendingUpTo(source parser.Source, amount *big.Int) (*b return nil, err } for i, allotmentItem := range source.Items { - err := s.trySendingExact(allotmentItem.From, allot[i]) + err := s.tryTakingExact(allotmentItem.From, allot[i]) if err != nil { return nil, err } @@ -686,11 +688,11 @@ func (s *programState) trySendingUpTo(source parser.Source, amount *big.Int) (*b return amount, nil case *parser.SourceCapped: - cap, err := evaluateExprAs(s, source.Cap, expectMonetaryOfAsset(s.CurrentAsset)) + cap, err := evaluateExprAs(s, source.Cap, expectMonetaryOfAsset(s.fundsQueue.asset)) if err != nil { return nil, err } - return s.trySendingUpTo(source.From, utils.NonNeg( + return s.tryTakingUpTo(source.From, utils.NonNeg( utils.MinBigInt(amount, cap), )) @@ -702,14 +704,14 @@ func (s *programState) trySendingUpTo(source parser.Source, amount *big.Int) (*b } -func (s *programState) receiveFrom(destination parser.Destination, amount *big.Int) InterpreterError { +func (s *programState) sendTo(destination parser.Destination, amount *big.Int) InterpreterError { switch destination := destination.(type) { case *parser.DestinationAccount: account, err := evaluateExprAs(s, destination.ValueExpr, expectAccount) if err != nil { return err } - s.pushReceiver(*account, amount) + s.sendToAccount(*account, amount) return nil case *parser.DestinationAllotment: @@ -726,7 +728,7 @@ func (s *programState) receiveFrom(destination parser.Destination, amount *big.I receivedTotal := big.NewInt(0) for i, allotmentItem := range destination.Items { amtToReceive := allot[i] - err := s.receiveFromKeptOrDest(allotmentItem.To, amtToReceive) + err := s.sendToKeptOrDest(allotmentItem.To, amtToReceive) if err != nil { return err } @@ -743,7 +745,7 @@ func (s *programState) receiveFrom(destination parser.Destination, amount *big.I return nil } - err := s.receiveFromKeptOrDest(keptOrDest, amountToReceive) + err := s.sendToKeptOrDest(keptOrDest, amountToReceive) if err != nil { return err } @@ -753,7 +755,7 @@ func (s *programState) receiveFrom(destination parser.Destination, amount *big.I for _, destinationClause := range destination.Clauses { - cap, err := evaluateExprAs(s, destinationClause.Cap, expectMonetaryOfAsset(s.CurrentAsset)) + cap, err := evaluateExprAs(s, destinationClause.Cap, expectMonetaryOfAsset(s.fundsQueue.asset)) if err != nil { return err } @@ -780,7 +782,7 @@ func (s *programState) receiveFrom(destination parser.Destination, amount *big.I return err } for _, destinationClause := range destination.Clauses { - cap, err := evaluateExprAs(s, destinationClause.Cap, expectMonetaryOfAsset(s.CurrentAsset)) + cap, err := evaluateExprAs(s, destinationClause.Cap, expectMonetaryOfAsset(s.fundsQueue.asset)) if err != nil { return err } @@ -788,12 +790,12 @@ func (s *programState) receiveFrom(destination parser.Destination, amount *big.I // if the clause cap is >= the amount we're trying to receive, only go through this branch switch cap.Cmp(amount) { case 0, 1: - return s.receiveFromKeptOrDest(destinationClause.To, amount) + return s.sendToKeptOrDest(destinationClause.To, amount) } // otherwise try next clause (keep looping) } - return s.receiveFromKeptOrDest(destination.Remaining, amount) + return s.sendToKeptOrDest(destination.Remaining, amount) default: utils.NonExhaustiveMatchPanic[any](destination) @@ -803,14 +805,14 @@ func (s *programState) receiveFrom(destination parser.Destination, amount *big.I const KEPT_ADDR = "" -func (s *programState) receiveFromKeptOrDest(keptOrDest parser.KeptOrDestination, amount *big.Int) InterpreterError { +func (s *programState) sendToKeptOrDest(keptOrDest parser.KeptOrDestination, amount *big.Int) InterpreterError { switch destinationTarget := keptOrDest.(type) { case *parser.DestinationKept: - s.pushReceiver(KEPT_ADDR, amount) + s.sendToAccount(KEPT_ADDR, amount) return nil case *parser.DestinationTo: - return s.receiveFrom(destinationTarget.Destination, amount) + return s.sendTo(destinationTarget.Destination, amount) default: utils.NonExhaustiveMatchPanic[any](destinationTarget) From a24050d20f8419fd6856ded47082f3fe5069eb94 Mon Sep 17 00:00:00 2001 From: Azorlogh Date: Mon, 13 Oct 2025 19:26:37 +0200 Subject: [PATCH 2/2] fundsQueue pulling: don't shift elements when there is no offset --- internal/interpreter/funds_queue.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/internal/interpreter/funds_queue.go b/internal/interpreter/funds_queue.go index e5cb9e35..fef089ef 100644 --- a/internal/interpreter/funds_queue.go +++ b/internal/interpreter/funds_queue.go @@ -78,7 +78,7 @@ func (s *fundsQueue) Pull(maxAmount *big.Int, color *string) []Sender { out := newFundsQueue([]Sender{}) offset := 0 - for maxAmount.Cmp(big.NewInt(0)) != 0 && len(s.senders) > offset { + for maxAmount.Sign() > 0 && len(s.senders) > offset { frontSender := s.senders[offset] @@ -91,7 +91,11 @@ func (s *fundsQueue) Pull(maxAmount *big.Int, color *string) []Sender { case -1: // not enough maxAmount.Sub(maxAmount, frontSender.Amount) out.Push(frontSender) - s.senders = slices.Delete(s.senders, offset, offset+1) + if offset == 0 { + s.senders = s.senders[1:] + } else { + s.senders = slices.Delete(s.senders, offset, offset+1) + } case 1: // more than enough out.Push(Sender{ Name: frontSender.Name, @@ -102,7 +106,11 @@ func (s *fundsQueue) Pull(maxAmount *big.Int, color *string) []Sender { return out.senders case 0: // exactly enough out.Push(s.senders[offset]) - s.senders = slices.Delete(s.senders, offset, offset+1) + if offset == 0 { + s.senders = s.senders[1:] + } else { + s.senders = slices.Delete(s.senders, offset, offset+1) + } return out.senders } }