Skip to content

Commit bd4b15b

Browse files
committed
feat: implemented + and - infix operators
1 parent 16a4ead commit bd4b15b

File tree

4 files changed

+335
-0
lines changed

4 files changed

+335
-0
lines changed

internal/interpreter/evaluate_expr.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,22 @@ func (st *programState) evaluateExpr(expr parser.ValueExpr) (Value, InterpreterE
4141
}
4242
}
4343
return value, nil
44+
45+
// TypeError
46+
case *parser.BinaryInfix:
47+
48+
switch expr.Operator {
49+
case parser.InfixOperatorPlus:
50+
return st.plusOp(expr.Left, expr.Right)
51+
52+
case parser.InfixOperatorMinus:
53+
return st.subOp(expr.Left, expr.Right)
54+
55+
default:
56+
utils.NonExhaustiveMatchPanic[any](expr.Operator)
57+
return nil, nil
58+
}
59+
4460
default:
4561
utils.NonExhaustiveMatchPanic[any](expr)
4662
return nil, nil
@@ -72,3 +88,39 @@ func (st *programState) evaluateExpressions(literals []parser.ValueExpr) ([]Valu
7288
}
7389
return values, nil
7490
}
91+
92+
func (st *programState) plusOp(left parser.ValueExpr, right parser.ValueExpr) (Value, InterpreterError) {
93+
leftValue, err := evaluateExprAs(st, left, expectOneOf(
94+
expectMapped(expectMonetary, func(m Monetary) opAdd {
95+
return m
96+
}),
97+
98+
// while "x.map(identity)" is the same as "x", just writing "expectNumber" would't typecheck
99+
expectMapped(expectNumber, func(bi big.Int) opAdd {
100+
return MonetaryInt(bi)
101+
}),
102+
))
103+
104+
if err != nil {
105+
return nil, err
106+
}
107+
108+
return (*leftValue).evalAdd(st, right)
109+
}
110+
111+
func (st *programState) subOp(left parser.ValueExpr, right parser.ValueExpr) (Value, InterpreterError) {
112+
leftValue, err := evaluateExprAs(st, left, expectOneOf(
113+
expectMapped(expectMonetary, func(m Monetary) opSub {
114+
return m
115+
}),
116+
expectMapped(expectNumber, func(bi big.Int) opSub {
117+
return MonetaryInt(bi)
118+
}),
119+
))
120+
121+
if err != nil {
122+
return nil, err
123+
}
124+
125+
return (*leftValue).evalSub(st, right)
126+
}

internal/interpreter/infix.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package interpreter
2+
3+
import (
4+
"math/big"
5+
6+
"github.com/formancehq/numscript/internal/parser"
7+
)
8+
9+
type opAdd interface {
10+
evalAdd(st *programState, other parser.ValueExpr) (Value, InterpreterError)
11+
}
12+
13+
var _ opAdd = (*MonetaryInt)(nil)
14+
var _ opAdd = (*Monetary)(nil)
15+
16+
func (m MonetaryInt) evalAdd(st *programState, other parser.ValueExpr) (Value, InterpreterError) {
17+
m1 := big.Int(m)
18+
m2, err := evaluateExprAs(st, other, expectNumber)
19+
if err != nil {
20+
return nil, err
21+
}
22+
23+
sum := new(big.Int).Add(&m1, m2)
24+
return MonetaryInt(*sum), nil
25+
}
26+
27+
func (m Monetary) evalAdd(st *programState, other parser.ValueExpr) (Value, InterpreterError) {
28+
m2, err := evaluateExprAs(st, other, expectMonetary)
29+
if err != nil {
30+
return nil, err
31+
}
32+
33+
if m.Asset != m2.Asset {
34+
return nil, MismatchedCurrencyError{
35+
Expected: m.Asset.String(),
36+
Got: m2.Asset.String(),
37+
}
38+
}
39+
40+
return Monetary{
41+
Asset: m.Asset,
42+
Amount: m.Amount.Add(m2.Amount),
43+
}, nil
44+
45+
}
46+
47+
type opSub interface {
48+
evalSub(st *programState, other parser.ValueExpr) (Value, InterpreterError)
49+
}
50+
51+
var _ opSub = (*MonetaryInt)(nil)
52+
var _ opSub = (*Monetary)(nil)
53+
54+
func (m MonetaryInt) evalSub(st *programState, other parser.ValueExpr) (Value, InterpreterError) {
55+
m1 := big.Int(m)
56+
m2, err := evaluateExprAs(st, other, expectNumber)
57+
if err != nil {
58+
return nil, err
59+
}
60+
sum := new(big.Int).Sub(&m1, m2)
61+
return MonetaryInt(*sum), nil
62+
}
63+
64+
func (m Monetary) evalSub(st *programState, other parser.ValueExpr) (Value, InterpreterError) {
65+
m2, err := evaluateExprAs(st, other, expectMonetary)
66+
if err != nil {
67+
return nil, err
68+
}
69+
70+
if m.Asset != m2.Asset {
71+
return nil, MismatchedCurrencyError{
72+
Expected: m.Asset.String(),
73+
Got: m2.Asset.String(),
74+
}
75+
}
76+
77+
return Monetary{
78+
Asset: m.Asset,
79+
Amount: m.Amount.Sub(m2.Amount),
80+
}, nil
81+
82+
}

internal/interpreter/interpreter_test.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3157,3 +3157,136 @@ func TestSaveFromAccount(t *testing.T) {
31573157
test(t, tc)
31583158
})
31593159
}
3160+
3161+
func TestAddMonetariesSameCurrency(t *testing.T) {
3162+
script := `
3163+
send [COIN 1] + [COIN 2] (
3164+
source = @world
3165+
destination = @dest
3166+
)
3167+
`
3168+
3169+
tc := NewTestCase()
3170+
tc.compile(t, script)
3171+
3172+
tc.expected = CaseResult{
3173+
Postings: []Posting{
3174+
{
3175+
Asset: "COIN",
3176+
Amount: big.NewInt(1 + 2),
3177+
Source: "world",
3178+
Destination: "dest",
3179+
},
3180+
},
3181+
}
3182+
test(t, tc)
3183+
}
3184+
3185+
func TestAddNumbers(t *testing.T) {
3186+
script := `
3187+
set_tx_meta("k", 1 + 2)
3188+
`
3189+
3190+
tc := NewTestCase()
3191+
tc.compile(t, script)
3192+
3193+
tc.expected = CaseResult{
3194+
TxMetadata: map[string]machine.Value{
3195+
"k": machine.NewMonetaryInt(1 + 2),
3196+
},
3197+
}
3198+
test(t, tc)
3199+
}
3200+
3201+
func TestAddNumbersInvalidRightType(t *testing.T) {
3202+
script := `
3203+
set_tx_meta("k", 1 + "not a number")
3204+
`
3205+
3206+
tc := NewTestCase()
3207+
tc.compile(t, script)
3208+
3209+
tc.expected = CaseResult{
3210+
Error: machine.TypeError{
3211+
Expected: "number",
3212+
Value: machine.String("not a number"),
3213+
},
3214+
}
3215+
test(t, tc)
3216+
}
3217+
3218+
func TestAddMonetariesDifferentCurrencies(t *testing.T) {
3219+
script := `
3220+
send [USD/2 1] + [EUR/2 2] (
3221+
source = @world
3222+
destination = @dest
3223+
)
3224+
`
3225+
3226+
tc := NewTestCase()
3227+
tc.compile(t, script)
3228+
3229+
tc.expected = CaseResult{
3230+
Postings: []Posting{},
3231+
Error: machine.MismatchedCurrencyError{
3232+
Expected: "USD/2",
3233+
Got: "EUR/2",
3234+
},
3235+
}
3236+
test(t, tc)
3237+
}
3238+
3239+
func TestAddInvalidLeftType(t *testing.T) {
3240+
script := `
3241+
set_tx_meta("k", EUR/2 + EUR/3)
3242+
`
3243+
3244+
tc := NewTestCase()
3245+
tc.compile(t, script)
3246+
3247+
tc.expected = CaseResult{
3248+
Postings: []Posting{},
3249+
Error: machine.TypeError{
3250+
Expected: "monetary|number",
3251+
Value: machine.Asset("EUR/2"),
3252+
},
3253+
}
3254+
test(t, tc)
3255+
}
3256+
3257+
func TestSubNumbers(t *testing.T) {
3258+
script := `
3259+
set_tx_meta("k", 10 - 1)
3260+
`
3261+
3262+
tc := NewTestCase()
3263+
tc.compile(t, script)
3264+
3265+
tc.expected = CaseResult{
3266+
Postings: []Posting{},
3267+
TxMetadata: map[string]machine.Value{
3268+
"k": machine.NewMonetaryInt(10 - 1),
3269+
},
3270+
}
3271+
test(t, tc)
3272+
}
3273+
3274+
func TestSubMonetaries(t *testing.T) {
3275+
script := `
3276+
set_tx_meta("k", [USD/2 10] - [USD/2 3])
3277+
`
3278+
3279+
tc := NewTestCase()
3280+
tc.compile(t, script)
3281+
3282+
tc.expected = CaseResult{
3283+
Postings: []Posting{},
3284+
TxMetadata: map[string]machine.Value{
3285+
"k": machine.Monetary{
3286+
Amount: machine.NewMonetaryInt(10 - 3),
3287+
Asset: "USD/2",
3288+
},
3289+
},
3290+
}
3291+
test(t, tc)
3292+
}

internal/interpreter/value.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,75 @@ func expectAnything(v Value, _ parser.Range) (*Value, InterpreterError) {
155155
return &v, nil
156156
}
157157

158+
func expectOneOf[T any](combinators ...func(v Value, r parser.Range) (*T, InterpreterError)) func(v Value, r parser.Range) (*T, InterpreterError) {
159+
return func(v Value, r parser.Range) (*T, InterpreterError) {
160+
if len(combinators) == 0 {
161+
// this should be unreachable
162+
panic("Invalid argument: no combinators given")
163+
}
164+
165+
var errs []TypeError
166+
for _, combinator := range combinators {
167+
out, err := combinator(v, r)
168+
if err == nil {
169+
return out, nil
170+
}
171+
172+
typeErr, ok := err.(TypeError)
173+
if !ok {
174+
return nil, err
175+
}
176+
errs = append(errs, typeErr)
177+
}
178+
179+
// e.g. typeErr.map(e => e.Expected).join("|")
180+
expected := ""
181+
for index, typeErr := range errs {
182+
if index != 0 {
183+
expected += "|"
184+
}
185+
expected += typeErr.Expected
186+
}
187+
188+
return nil, TypeError{
189+
Range: r,
190+
Value: v,
191+
Expected: expected,
192+
}
193+
}
194+
}
195+
196+
func expectMapped[T any, U any](
197+
combinator func(v Value, r parser.Range) (*T, InterpreterError),
198+
mapper func(value T) U,
199+
) func(v Value, r parser.Range) (*U, InterpreterError) {
200+
return func(v Value, r parser.Range) (*U, InterpreterError) {
201+
out, err := combinator(v, r)
202+
if err != nil {
203+
return nil, err
204+
}
205+
mapped := mapper(*out)
206+
return &mapped, nil
207+
}
208+
}
209+
158210
func NewMonetaryInt(n int64) MonetaryInt {
159211
bi := big.NewInt(n)
160212
return MonetaryInt(*bi)
161213
}
214+
215+
func (m MonetaryInt) Add(other MonetaryInt) MonetaryInt {
216+
bi := big.Int(m)
217+
otherBi := big.Int(other)
218+
219+
sum := new(big.Int).Add(&bi, &otherBi)
220+
return MonetaryInt(*sum)
221+
}
222+
223+
func (m MonetaryInt) Sub(other MonetaryInt) MonetaryInt {
224+
bi := big.Int(m)
225+
otherBi := big.Int(other)
226+
227+
sum := new(big.Int).Sub(&bi, &otherBi)
228+
return MonetaryInt(*sum)
229+
}

0 commit comments

Comments
 (0)