From 603cbd6906ee3a92ba8789d4c3c61ef86aae2a90 Mon Sep 17 00:00:00 2001 From: Maxence Maireaux Date: Thu, 4 Jun 2026 13:29:32 +0200 Subject: [PATCH] fix: reject duplicate remaining allotments --- internal/interpreter/interpreter.go | 6 +++++- internal/interpreter/interpreter_error.go | 8 ++++++++ internal/interpreter/interpreter_test.go | 18 ++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/internal/interpreter/interpreter.go b/internal/interpreter/interpreter.go index 94b634af..2203cd8a 100644 --- a/internal/interpreter/interpreter.go +++ b/internal/interpreter/interpreter.go @@ -1029,9 +1029,13 @@ func (s *programState) makeAllotment(monetary *big.Int, items []parser.Allotment allotments = append(allotments, rat) case *parser.RemainingAllotment: + if remainingAllotmentIndex != -1 { + return nil, InvalidRemainingAllotment{ + Range: allotment.Range, + } + } remainingAllotmentIndex = i allotments = append(allotments, new(big.Rat)) - // TODO check there are not duplicate remaining clause } } diff --git a/internal/interpreter/interpreter_error.go b/internal/interpreter/interpreter_error.go index 3d8527c3..258c6766 100644 --- a/internal/interpreter/interpreter_error.go +++ b/internal/interpreter/interpreter_error.go @@ -153,6 +153,14 @@ func (e InvalidAllotmentInSendAll) Error() string { return "cannot take all balance of an allotment source" } +type InvalidRemainingAllotment struct { + parser.Range +} + +func (e InvalidRemainingAllotment) Error() string { + return "Only one 'remaining' clause is allowed in an allotment expression" +} + type DivideByZero struct { parser.Range Numerator *big.Int diff --git a/internal/interpreter/interpreter_test.go b/internal/interpreter/interpreter_test.go index 651d17b7..fa4a944d 100644 --- a/internal/interpreter/interpreter_test.go +++ b/internal/interpreter/interpreter_test.go @@ -419,6 +419,24 @@ func TestInvalidDestinationAllotmentSum(t *testing.T) { test(t, tc) } +func TestRejectsDuplicateRemainingAllotments(t *testing.T) { + tc := NewTestCase() + src := tc.compile(t, `send [COIN 100] ( + source = { + remaining from @a + remaining from @b + } + destination = @dest + )`) + + tc.expected = CaseResult{ + Error: machine.InvalidRemainingAllotment{ + Range: parser.RangeOfIndexed(src, "remaining", 1), + }, + } + test(t, tc) +} + func TestSourceAllotmentInvalidAmt(t *testing.T) { tc := NewTestCase() tc.compile(t, `send [COIN 100] (