Skip to content

Commit

Permalink
feat: implemented + and - infix operators
Browse files Browse the repository at this point in the history
  • Loading branch information
ascandone committed Nov 26, 2024
1 parent 16a4ead commit bd4b15b
Show file tree
Hide file tree
Showing 4 changed files with 335 additions and 0 deletions.
52 changes: 52 additions & 0 deletions internal/interpreter/evaluate_expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,22 @@ func (st *programState) evaluateExpr(expr parser.ValueExpr) (Value, InterpreterE
}
}
return value, nil

// TypeError
case *parser.BinaryInfix:

switch expr.Operator {
case parser.InfixOperatorPlus:
return st.plusOp(expr.Left, expr.Right)

case parser.InfixOperatorMinus:
return st.subOp(expr.Left, expr.Right)

default:
utils.NonExhaustiveMatchPanic[any](expr.Operator)
return nil, nil

Check warning on line 57 in internal/interpreter/evaluate_expr.go

View check run for this annotation

Codecov / codecov/patch

internal/interpreter/evaluate_expr.go#L55-L57

Added lines #L55 - L57 were not covered by tests
}

default:
utils.NonExhaustiveMatchPanic[any](expr)
return nil, nil

Check warning on line 62 in internal/interpreter/evaluate_expr.go

View check run for this annotation

Codecov / codecov/patch

internal/interpreter/evaluate_expr.go#L60-L62

Added lines #L60 - L62 were not covered by tests
Expand Down Expand Up @@ -72,3 +88,39 @@ func (st *programState) evaluateExpressions(literals []parser.ValueExpr) ([]Valu
}
return values, nil
}

func (st *programState) plusOp(left parser.ValueExpr, right parser.ValueExpr) (Value, InterpreterError) {
leftValue, err := evaluateExprAs(st, left, expectOneOf(
expectMapped(expectMonetary, func(m Monetary) opAdd {
return m
}),

// while "x.map(identity)" is the same as "x", just writing "expectNumber" would't typecheck
expectMapped(expectNumber, func(bi big.Int) opAdd {
return MonetaryInt(bi)
}),
))

if err != nil {
return nil, err
}

return (*leftValue).evalAdd(st, right)
}

func (st *programState) subOp(left parser.ValueExpr, right parser.ValueExpr) (Value, InterpreterError) {
leftValue, err := evaluateExprAs(st, left, expectOneOf(
expectMapped(expectMonetary, func(m Monetary) opSub {
return m
}),
expectMapped(expectNumber, func(bi big.Int) opSub {
return MonetaryInt(bi)
}),
))

if err != nil {
return nil, err
}

Check warning on line 123 in internal/interpreter/evaluate_expr.go

View check run for this annotation

Codecov / codecov/patch

internal/interpreter/evaluate_expr.go#L122-L123

Added lines #L122 - L123 were not covered by tests

return (*leftValue).evalSub(st, right)
}
82 changes: 82 additions & 0 deletions internal/interpreter/infix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package interpreter

import (
"math/big"

"github.com/formancehq/numscript/internal/parser"
)

type opAdd interface {
evalAdd(st *programState, other parser.ValueExpr) (Value, InterpreterError)
}

var _ opAdd = (*MonetaryInt)(nil)
var _ opAdd = (*Monetary)(nil)

func (m MonetaryInt) evalAdd(st *programState, other parser.ValueExpr) (Value, InterpreterError) {
m1 := big.Int(m)
m2, err := evaluateExprAs(st, other, expectNumber)
if err != nil {
return nil, err
}

sum := new(big.Int).Add(&m1, m2)
return MonetaryInt(*sum), nil
}

func (m Monetary) evalAdd(st *programState, other parser.ValueExpr) (Value, InterpreterError) {
m2, err := evaluateExprAs(st, other, expectMonetary)
if err != nil {
return nil, err
}

Check warning on line 31 in internal/interpreter/infix.go

View check run for this annotation

Codecov / codecov/patch

internal/interpreter/infix.go#L30-L31

Added lines #L30 - L31 were not covered by tests

if m.Asset != m2.Asset {
return nil, MismatchedCurrencyError{
Expected: m.Asset.String(),
Got: m2.Asset.String(),
}
}

return Monetary{
Asset: m.Asset,
Amount: m.Amount.Add(m2.Amount),
}, nil

}

type opSub interface {
evalSub(st *programState, other parser.ValueExpr) (Value, InterpreterError)
}

var _ opSub = (*MonetaryInt)(nil)
var _ opSub = (*Monetary)(nil)

func (m MonetaryInt) evalSub(st *programState, other parser.ValueExpr) (Value, InterpreterError) {
m1 := big.Int(m)
m2, err := evaluateExprAs(st, other, expectNumber)
if err != nil {
return nil, err
}

Check warning on line 59 in internal/interpreter/infix.go

View check run for this annotation

Codecov / codecov/patch

internal/interpreter/infix.go#L58-L59

Added lines #L58 - L59 were not covered by tests
sum := new(big.Int).Sub(&m1, m2)
return MonetaryInt(*sum), nil
}

func (m Monetary) evalSub(st *programState, other parser.ValueExpr) (Value, InterpreterError) {
m2, err := evaluateExprAs(st, other, expectMonetary)
if err != nil {
return nil, err
}

Check warning on line 68 in internal/interpreter/infix.go

View check run for this annotation

Codecov / codecov/patch

internal/interpreter/infix.go#L67-L68

Added lines #L67 - L68 were not covered by tests

if m.Asset != m2.Asset {
return nil, MismatchedCurrencyError{
Expected: m.Asset.String(),
Got: m2.Asset.String(),
}
}

Check warning on line 75 in internal/interpreter/infix.go

View check run for this annotation

Codecov / codecov/patch

internal/interpreter/infix.go#L71-L75

Added lines #L71 - L75 were not covered by tests

return Monetary{
Asset: m.Asset,
Amount: m.Amount.Sub(m2.Amount),
}, nil

}
133 changes: 133 additions & 0 deletions internal/interpreter/interpreter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3157,3 +3157,136 @@ func TestSaveFromAccount(t *testing.T) {
test(t, tc)
})
}

func TestAddMonetariesSameCurrency(t *testing.T) {
script := `
send [COIN 1] + [COIN 2] (
source = @world
destination = @dest
)
`

tc := NewTestCase()
tc.compile(t, script)

tc.expected = CaseResult{
Postings: []Posting{
{
Asset: "COIN",
Amount: big.NewInt(1 + 2),
Source: "world",
Destination: "dest",
},
},
}
test(t, tc)
}

func TestAddNumbers(t *testing.T) {
script := `
set_tx_meta("k", 1 + 2)
`

tc := NewTestCase()
tc.compile(t, script)

tc.expected = CaseResult{
TxMetadata: map[string]machine.Value{
"k": machine.NewMonetaryInt(1 + 2),
},
}
test(t, tc)
}

func TestAddNumbersInvalidRightType(t *testing.T) {
script := `
set_tx_meta("k", 1 + "not a number")
`

tc := NewTestCase()
tc.compile(t, script)

tc.expected = CaseResult{
Error: machine.TypeError{
Expected: "number",
Value: machine.String("not a number"),
},
}
test(t, tc)
}

func TestAddMonetariesDifferentCurrencies(t *testing.T) {
script := `
send [USD/2 1] + [EUR/2 2] (
source = @world
destination = @dest
)
`

tc := NewTestCase()
tc.compile(t, script)

tc.expected = CaseResult{
Postings: []Posting{},
Error: machine.MismatchedCurrencyError{
Expected: "USD/2",
Got: "EUR/2",
},
}
test(t, tc)
}

func TestAddInvalidLeftType(t *testing.T) {
script := `
set_tx_meta("k", EUR/2 + EUR/3)
`

tc := NewTestCase()
tc.compile(t, script)

tc.expected = CaseResult{
Postings: []Posting{},
Error: machine.TypeError{
Expected: "monetary|number",
Value: machine.Asset("EUR/2"),
},
}
test(t, tc)
}

func TestSubNumbers(t *testing.T) {
script := `
set_tx_meta("k", 10 - 1)
`

tc := NewTestCase()
tc.compile(t, script)

tc.expected = CaseResult{
Postings: []Posting{},
TxMetadata: map[string]machine.Value{
"k": machine.NewMonetaryInt(10 - 1),
},
}
test(t, tc)
}

func TestSubMonetaries(t *testing.T) {
script := `
set_tx_meta("k", [USD/2 10] - [USD/2 3])
`

tc := NewTestCase()
tc.compile(t, script)

tc.expected = CaseResult{
Postings: []Posting{},
TxMetadata: map[string]machine.Value{
"k": machine.Monetary{
Amount: machine.NewMonetaryInt(10 - 3),
Asset: "USD/2",
},
},
}
test(t, tc)
}
68 changes: 68 additions & 0 deletions internal/interpreter/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,75 @@ func expectAnything(v Value, _ parser.Range) (*Value, InterpreterError) {
return &v, nil
}

func expectOneOf[T any](combinators ...func(v Value, r parser.Range) (*T, InterpreterError)) func(v Value, r parser.Range) (*T, InterpreterError) {
return func(v Value, r parser.Range) (*T, InterpreterError) {
if len(combinators) == 0 {
// this should be unreachable
panic("Invalid argument: no combinators given")

Check warning on line 162 in internal/interpreter/value.go

View check run for this annotation

Codecov / codecov/patch

internal/interpreter/value.go#L161-L162

Added lines #L161 - L162 were not covered by tests
}

var errs []TypeError
for _, combinator := range combinators {
out, err := combinator(v, r)
if err == nil {
return out, nil
}

typeErr, ok := err.(TypeError)
if !ok {
return nil, err
}

Check warning on line 175 in internal/interpreter/value.go

View check run for this annotation

Codecov / codecov/patch

internal/interpreter/value.go#L174-L175

Added lines #L174 - L175 were not covered by tests
errs = append(errs, typeErr)
}

// e.g. typeErr.map(e => e.Expected).join("|")
expected := ""
for index, typeErr := range errs {
if index != 0 {
expected += "|"
}
expected += typeErr.Expected
}

return nil, TypeError{
Range: r,
Value: v,
Expected: expected,
}
}
}

func expectMapped[T any, U any](
combinator func(v Value, r parser.Range) (*T, InterpreterError),
mapper func(value T) U,
) func(v Value, r parser.Range) (*U, InterpreterError) {
return func(v Value, r parser.Range) (*U, InterpreterError) {
out, err := combinator(v, r)
if err != nil {
return nil, err
}
mapped := mapper(*out)
return &mapped, nil
}
}

func NewMonetaryInt(n int64) MonetaryInt {
bi := big.NewInt(n)
return MonetaryInt(*bi)
}

func (m MonetaryInt) Add(other MonetaryInt) MonetaryInt {
bi := big.Int(m)
otherBi := big.Int(other)

sum := new(big.Int).Add(&bi, &otherBi)
return MonetaryInt(*sum)
}

func (m MonetaryInt) Sub(other MonetaryInt) MonetaryInt {
bi := big.Int(m)
otherBi := big.Int(other)

sum := new(big.Int).Sub(&bi, &otherBi)
return MonetaryInt(*sum)
}

0 comments on commit bd4b15b

Please sign in to comment.