Skip to content

Commit b0d00e9

Browse files
committed
feat: implemented + and - infix operators
1 parent 2834e91 commit b0d00e9

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
@@ -3206,3 +3206,136 @@ func TestSaveFromAccount(t *testing.T) {
32063206
test(t, tc)
32073207
})
32083208
}
3209+
3210+
func TestAddMonetariesSameCurrency(t *testing.T) {
3211+
script := `
3212+
send [COIN 1] + [COIN 2] (
3213+
source = @world
3214+
destination = @dest
3215+
)
3216+
`
3217+
3218+
tc := NewTestCase()
3219+
tc.compile(t, script)
3220+
3221+
tc.expected = CaseResult{
3222+
Postings: []Posting{
3223+
{
3224+
Asset: "COIN",
3225+
Amount: big.NewInt(1 + 2),
3226+
Source: "world",
3227+
Destination: "dest",
3228+
},
3229+
},
3230+
}
3231+
test(t, tc)
3232+
}
3233+
3234+
func TestAddNumbers(t *testing.T) {
3235+
script := `
3236+
set_tx_meta("k", 1 + 2)
3237+
`
3238+
3239+
tc := NewTestCase()
3240+
tc.compile(t, script)
3241+
3242+
tc.expected = CaseResult{
3243+
TxMetadata: map[string]machine.Value{
3244+
"k": machine.NewMonetaryInt(1 + 2),
3245+
},
3246+
}
3247+
test(t, tc)
3248+
}
3249+
3250+
func TestAddNumbersInvalidRightType(t *testing.T) {
3251+
script := `
3252+
set_tx_meta("k", 1 + "not a number")
3253+
`
3254+
3255+
tc := NewTestCase()
3256+
tc.compile(t, script)
3257+
3258+
tc.expected = CaseResult{
3259+
Error: machine.TypeError{
3260+
Expected: "number",
3261+
Value: machine.String("not a number"),
3262+
},
3263+
}
3264+
test(t, tc)
3265+
}
3266+
3267+
func TestAddMonetariesDifferentCurrencies(t *testing.T) {
3268+
script := `
3269+
send [USD/2 1] + [EUR/2 2] (
3270+
source = @world
3271+
destination = @dest
3272+
)
3273+
`
3274+
3275+
tc := NewTestCase()
3276+
tc.compile(t, script)
3277+
3278+
tc.expected = CaseResult{
3279+
Postings: []Posting{},
3280+
Error: machine.MismatchedCurrencyError{
3281+
Expected: "USD/2",
3282+
Got: "EUR/2",
3283+
},
3284+
}
3285+
test(t, tc)
3286+
}
3287+
3288+
func TestAddInvalidLeftType(t *testing.T) {
3289+
script := `
3290+
set_tx_meta("k", EUR/2 + EUR/3)
3291+
`
3292+
3293+
tc := NewTestCase()
3294+
tc.compile(t, script)
3295+
3296+
tc.expected = CaseResult{
3297+
Postings: []Posting{},
3298+
Error: machine.TypeError{
3299+
Expected: "monetary|number",
3300+
Value: machine.Asset("EUR/2"),
3301+
},
3302+
}
3303+
test(t, tc)
3304+
}
3305+
3306+
func TestSubNumbers(t *testing.T) {
3307+
script := `
3308+
set_tx_meta("k", 10 - 1)
3309+
`
3310+
3311+
tc := NewTestCase()
3312+
tc.compile(t, script)
3313+
3314+
tc.expected = CaseResult{
3315+
Postings: []Posting{},
3316+
TxMetadata: map[string]machine.Value{
3317+
"k": machine.NewMonetaryInt(10 - 1),
3318+
},
3319+
}
3320+
test(t, tc)
3321+
}
3322+
3323+
func TestSubMonetaries(t *testing.T) {
3324+
script := `
3325+
set_tx_meta("k", [USD/2 10] - [USD/2 3])
3326+
`
3327+
3328+
tc := NewTestCase()
3329+
tc.compile(t, script)
3330+
3331+
tc.expected = CaseResult{
3332+
Postings: []Posting{},
3333+
TxMetadata: map[string]machine.Value{
3334+
"k": machine.Monetary{
3335+
Amount: machine.NewMonetaryInt(10 - 3),
3336+
Asset: "USD/2",
3337+
},
3338+
},
3339+
}
3340+
test(t, tc)
3341+
}

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)