mirror of
https://github.com/hanzoai/numscript.git
synced 2026-10-30 18:26:57 +00:00
Modules declare go 1.27.1 and the build images follow. go fix applied the modernizers, and the interpreter's parseArg, evaluateExprAs and evaluateOptExprAs are now generic methods on the parser and program state they take. omitzero is not applied, since it changes what the JSON says. Co-Authored-By: Claude Opus 5 <[email protected]>
259 lines
6.0 KiB
Go
259 lines
6.0 KiB
Go
package interpreter
|
|
|
|
import (
|
|
"math/big"
|
|
"strings"
|
|
|
|
"github.com/formancehq/numscript/internal/flags"
|
|
"github.com/formancehq/numscript/internal/parser"
|
|
"github.com/formancehq/numscript/internal/utils"
|
|
)
|
|
|
|
func (st *programState) evaluateExpr(expr parser.ValueExpr) (Value, InterpreterError) {
|
|
switch expr := expr.(type) {
|
|
case *parser.AssetLiteral:
|
|
return Asset(expr.Asset), nil
|
|
case *parser.AccountInterpLiteral:
|
|
var parts []string
|
|
for _, part := range expr.Parts {
|
|
switch part := part.(type) {
|
|
case parser.AccountTextPart:
|
|
parts = append(parts, part.Name)
|
|
case *parser.Variable:
|
|
err := st.checkFeatureFlag(flags.ExperimentalAccountInterpolationFlag)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
value, err := st.evaluateExpr(part)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
strValue, err := castToString(value, expr.Range)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
parts = append(parts, strValue)
|
|
}
|
|
}
|
|
name := strings.Join(parts, "")
|
|
return NewAccountAddress(name)
|
|
|
|
case *parser.StringLiteral:
|
|
return String(expr.String), nil
|
|
case *parser.PercentageLiteral:
|
|
return Portion(*expr.ToRatio()), nil
|
|
case *parser.NumberLiteral:
|
|
return MonetaryInt(*expr.Number), nil
|
|
case *parser.MonetaryLiteral:
|
|
asset, err := st.evaluateExprAs(expr.Asset, expectAsset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
amount, err := st.evaluateExprAs(expr.Amount, expectNumber)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return Monetary{Asset: Asset(*asset), Amount: MonetaryInt(*amount)}, nil
|
|
|
|
case *parser.Variable:
|
|
value, ok := st.ParsedVars[expr.Name]
|
|
if !ok {
|
|
return nil, UnboundVariableErr{
|
|
Name: expr.Name,
|
|
Range: expr.Range,
|
|
}
|
|
}
|
|
return value, nil
|
|
|
|
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)
|
|
|
|
case parser.InfixOperatorDiv:
|
|
return st.divOp(expr.Range, expr.Left, expr.Right)
|
|
|
|
default:
|
|
utils.NonExhaustiveMatchPanic[any](expr.Operator)
|
|
return nil, nil
|
|
}
|
|
|
|
case *parser.Prefix:
|
|
switch expr.Operator {
|
|
case parser.PrefixOperatorMinus:
|
|
return st.unaryNegOp(expr.Expr)
|
|
|
|
default:
|
|
utils.NonExhaustiveMatchPanic[any](expr.Operator)
|
|
return nil, nil
|
|
}
|
|
|
|
case *parser.FnCall:
|
|
if !st.varOriginPosition {
|
|
err := st.checkFeatureFlag(flags.ExperimentalMidScriptFunctionCall)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return st.handleFnCall(nil, *expr)
|
|
|
|
default:
|
|
utils.NonExhaustiveMatchPanic[any](expr)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
func (st *programState) evaluateOptExprAs[T any](expr parser.ValueExpr, expect func(Value, parser.Range) (*T, InterpreterError)) (*T, InterpreterError) {
|
|
if expr == nil {
|
|
return nil, nil
|
|
}
|
|
return st.evaluateExprAs(expr, expect)
|
|
}
|
|
|
|
func (st *programState) evaluateExprAs[T any](expr parser.ValueExpr, expect func(Value, parser.Range) (*T, InterpreterError)) (*T, InterpreterError) {
|
|
value, err := st.evaluateExpr(expr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res, err := expect(value, expr.GetRange())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func (st *programState) evaluateExpressions(literals []parser.ValueExpr) ([]Value, InterpreterError) {
|
|
var values []Value
|
|
for _, argLit := range literals {
|
|
value, err := st.evaluateExpr(argLit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
values = append(values, value)
|
|
}
|
|
return values, nil
|
|
}
|
|
|
|
func (s *programState) evaluateColor(colorExpr parser.ValueExpr) (*string, InterpreterError) {
|
|
color, err := s.evaluateOptExprAs(colorExpr, expectString)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if color == nil {
|
|
col := ""
|
|
return &col, nil
|
|
}
|
|
|
|
isValidColor := colorRe.Match([]byte(*color))
|
|
if !isValidColor {
|
|
return nil, InvalidColor{
|
|
Range: colorExpr.GetRange(),
|
|
Color: *color,
|
|
}
|
|
}
|
|
|
|
return color, nil
|
|
}
|
|
|
|
func (st *programState) plusOp(left parser.ValueExpr, right parser.ValueExpr) (Value, InterpreterError) {
|
|
leftValue, err := st.evaluateExprAs(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 := st.evaluateExprAs(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
|
|
}
|
|
|
|
return (*leftValue).evalSub(st, right)
|
|
}
|
|
|
|
func (st *programState) divOp(rng parser.Range, left parser.ValueExpr, right parser.ValueExpr) (Value, InterpreterError) {
|
|
leftValue, err := st.evaluateExprAs(left, expectNumber)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rightValue, err := st.evaluateExprAs(right, expectNumber)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if rightValue.Cmp(big.NewInt(0)) == 0 {
|
|
return nil, DivideByZero{
|
|
Range: rng,
|
|
Numerator: leftValue,
|
|
}
|
|
}
|
|
|
|
rat := new(big.Rat).SetFrac(leftValue, rightValue)
|
|
|
|
return Portion(*rat), nil
|
|
}
|
|
|
|
func (st *programState) unaryNegOp(expr parser.ValueExpr) (Value, InterpreterError) {
|
|
evExpr, err := st.evaluateExprAs(expr, expectOneOf(
|
|
expectMapped(expectMonetary, func(m Monetary) opNeg {
|
|
return m
|
|
}),
|
|
|
|
// while "x.map(identity)" is the same as "x", just writing "expectNumber" would't typecheck
|
|
expectMapped(expectNumber, func(bi big.Int) opNeg {
|
|
return MonetaryInt(bi)
|
|
}),
|
|
))
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return (*evExpr).evalNeg(st)
|
|
}
|
|
|
|
func castToString(v Value, rng parser.Range) (string, InterpreterError) {
|
|
switch v := v.(type) {
|
|
case AccountAddress:
|
|
return v.String(), nil
|
|
case String:
|
|
return v.String(), nil
|
|
case MonetaryInt:
|
|
return v.String(), nil
|
|
|
|
default:
|
|
// No asset nor ratio can be implicitly cast to string
|
|
return "", CannotCastToString{Value: v, Range: rng}
|
|
}
|
|
}
|