diff --git a/accounts/accounts.go b/accounts/accounts.go deleted file mode 100644 index c99b6f34..00000000 --- a/accounts/accounts.go +++ /dev/null @@ -1,27 +0,0 @@ -package accounts - -import "github.com/formancehq/numscript/internal/interpreter" - -type ( - InvolvedAccountExpr = interpreter.InvolvedAccountExpr - InvolvedAccount = interpreter.InvolvedAccount - InvolvedMeta = interpreter.InvolvedMeta - - AssetLiteral = interpreter.AssetLiteral - AccountLiteral = interpreter.AccountLiteral - MakeMonetary = interpreter.MakeMonetary - NumberLiteral = interpreter.NumberLiteral - StringLiteral = interpreter.StringLiteral - Add = interpreter.Add - ConcatAccount = interpreter.ConcatAccount - Sub = interpreter.Sub - Div = interpreter.Div - SubPrefix = interpreter.SubPrefix - FnMeta = interpreter.FnMeta - GetAmount = interpreter.GetAmount - GetAsset = interpreter.GetAsset - GetBalance = interpreter.GetBalance - GetOverdraft = interpreter.GetOverdraft -) - -var IsValidCall = interpreter.IsValidCall diff --git a/internal/interpreter/get_involved_accounts.go b/internal/interpreter/get_involved_accounts.go deleted file mode 100644 index d0aa392e..00000000 --- a/internal/interpreter/get_involved_accounts.go +++ /dev/null @@ -1,689 +0,0 @@ -package interpreter - -import ( - "fmt" - "math/big" - - "github.com/formancehq/numscript/internal/analysis" - "github.com/formancehq/numscript/internal/flags" - "github.com/formancehq/numscript/internal/parser" -) - -type InvolvedAccountExpr interface{ involvedAccountExpr() } - -type ( - AssetLiteral struct { - Asset string - } - AccountLiteral struct { - Account string - } - MakeMonetary struct { - Asset InvolvedAccountExpr - Amount InvolvedAccountExpr - } - NumberLiteral struct { - Amount *big.Int - } - StringLiteral struct { - String string - } - ConcatAccount struct { - Left InvolvedAccountExpr - Right InvolvedAccountExpr - } - Add struct { - Left InvolvedAccountExpr - Right InvolvedAccountExpr - } - Sub struct { - Left InvolvedAccountExpr - Right InvolvedAccountExpr - } - Div struct { - Left InvolvedAccountExpr - Right InvolvedAccountExpr - } - SubPrefix struct { - Expr InvolvedAccountExpr - } - FnMeta struct { - ExpectedType string - Account InvolvedAccountExpr - Key InvolvedAccountExpr - } - GetAmount struct { - Monetary InvolvedAccountExpr - } - GetAsset struct { - Monetary InvolvedAccountExpr - } - GetBalance struct { - Account InvolvedAccountExpr - Asset InvolvedAccountExpr - } - GetOverdraft struct { - Account InvolvedAccountExpr - Asset InvolvedAccountExpr - } -) - -func (AssetLiteral) involvedAccountExpr() {} -func (MakeMonetary) involvedAccountExpr() {} -func (AccountLiteral) involvedAccountExpr() {} -func (NumberLiteral) involvedAccountExpr() {} -func (StringLiteral) involvedAccountExpr() {} -func (Add) involvedAccountExpr() {} -func (ConcatAccount) involvedAccountExpr() {} -func (Sub) involvedAccountExpr() {} -func (Div) involvedAccountExpr() {} -func (SubPrefix) involvedAccountExpr() {} -func (FnMeta) involvedAccountExpr() {} -func (GetAmount) involvedAccountExpr() {} -func (GetAsset) involvedAccountExpr() {} -func (GetBalance) involvedAccountExpr() {} -func (GetOverdraft) involvedAccountExpr() {} - -type InvolvedAccount struct { - AccountExpr InvolvedAccountExpr - AssetExpr InvolvedAccountExpr -} - -type InvolvedMeta struct { - Account InvolvedAccountExpr - Key InvolvedAccountExpr - // `Write` is nil when we're not writing data - Write InvolvedAccountExpr -} - -type involvedAccountsAnalysisState struct { - evaluatedVars map[string]InvolvedAccountExpr - currentAsset InvolvedAccountExpr - involvedAccounts []InvolvedAccount - involvedMeta []InvolvedMeta -} - -// A version of parseVar that returns involved account expr instead -func parseVarToInvolvedAccount(type_ string, rawValue string, r parser.Range) (InvolvedAccountExpr, InterpreterError) { - val, err := parseVar(type_, rawValue, r) - if err != nil { - return nil, err - } - - switch val := val.(type) { - case String: - return StringLiteral{String: string(val)}, nil - case AccountAddress: - return AccountLiteral{Account: string(val)}, nil - case Asset: - return AssetLiteral{Asset: string(val)}, nil - - case MonetaryInt: - bi := big.Int(val) - return NumberLiteral{Amount: &bi}, nil - - case Monetary: - bi := big.Int(val.Amount) - return MakeMonetary{ - Asset: AssetLiteral{Asset: string(val.Asset)}, - Amount: NumberLiteral{Amount: &bi}, - }, nil - - case Portion: - rat := big.Rat(val) - left := NumberLiteral{Amount: rat.Num()} - right := NumberLiteral{Amount: rat.Denom()} - return Div{Left: left, Right: right}, nil - } - - return nil, InvalidTypeErr{Range: r, Name: fmt.Sprintf("%T", val)} -} - -func GetInvolvedAccounts(vars VariablesMap, program parser.Program) ([]InvolvedAccount, []InvolvedMeta, InterpreterError) { - st := involvedAccountsAnalysisState{ - evaluatedVars: make(map[string]InvolvedAccountExpr), - } - if program.Vars != nil { - if err := st.parseVars(program.Vars.Declarations, vars); err != nil { - return nil, nil, err - } - } - - for _, stmt := range program.Statements { - switch stmt := stmt.(type) { - case *parser.SendStatement: - if err := st.evalSendStmt(*stmt); err != nil { - return nil, nil, err - } - - case *parser.SaveStatement: - if err := st.evalSaveStmt(*stmt); err != nil { - return nil, nil, err - } - - case *parser.FnCall: - switch stmt.Caller.Name { - case analysis.FnSetTxMeta: - // we can safely ignore this - - case analysis.FnSetAccountMeta: - if len(stmt.Args) != 3 { - return nil, nil, BadArityErr{Range: stmt.Range, ExpectedArity: 3, GivenArguments: len(stmt.Args)} - } - acc, err := st.evalExpr(stmt.Args[0]) - if err != nil { - return nil, nil, err - } - key, err := st.evalExpr(stmt.Args[1]) - if err != nil { - return nil, nil, err - } - written, err := st.evalExpr(stmt.Args[2]) - if err != nil { - return nil, nil, err - } - st.involvedMeta = append(st.involvedMeta, InvolvedMeta{ - Account: acc, - Key: key, - Write: written, - }) - } - } - } - - return st.involvedAccounts, st.involvedMeta, nil -} - -func (s *involvedAccountsAnalysisState) parseVars(varDeclrs []parser.VarDeclaration, rawVars map[string]string) InterpreterError { - for _, varsDecl := range varDeclrs { - if varsDecl.Origin == nil { - raw, ok := rawVars[varsDecl.Name.Name] - if !ok { - return MissingVariableErr{Name: varsDecl.Name.Name} - } - - parsed, err := parseVarToInvolvedAccount(varsDecl.Type.Name, raw, varsDecl.Type.Range) - if err != nil { - return err - } - s.evaluatedVars[varsDecl.Name.Name] = parsed - } else { - value, err := s.evalVar(*varsDecl.Origin, varsDecl.Type.Name) - if err != nil { - return err - } - s.evaluatedVars[varsDecl.Name.Name] = value - } - } - return nil -} - -func (st *involvedAccountsAnalysisState) evalSaveStmt(stmt parser.SaveStatement) InterpreterError { - account, err := st.evalExpr(stmt.Account) - if err != nil { - return err - } - - switch sentValue := stmt.SentValue.(type) { - case *parser.SentValueAll: - asset, err := st.evalExpr(sentValue.Asset) - if err != nil { - return err - } - st.involvedAccounts = append(st.involvedAccounts, InvolvedAccount{ - AccountExpr: account, - AssetExpr: asset, - }) - - case *parser.SentValueLiteral: - monetary, err := st.evalExpr(sentValue.Monetary) - if err != nil { - return err - } - asset := foldedGetAsset(monetary) - st.involvedAccounts = append(st.involvedAccounts, InvolvedAccount{ - AccountExpr: account, - AssetExpr: asset, - }) - } - return nil -} - -func (st *involvedAccountsAnalysisState) evalSendStmt(stmt parser.SendStatement) InterpreterError { - switch sentValue := stmt.SentValue.(type) { - case *parser.SentValueAll: - asset, err := st.evalExpr(sentValue.Asset) - if err != nil { - return err - } - st.currentAsset = asset - if err := st.evalSrc(stmt.Source); err != nil { - return err - } - return st.evalDest(stmt.Destination) - - case *parser.SentValueLiteral: - monetary, err := st.evalExpr(sentValue.Monetary) - if err != nil { - return err - } - st.currentAsset = foldedGetAsset(monetary) - if err := st.evalSrc(stmt.Source); err != nil { - return err - } - return st.evalDest(stmt.Destination) - } - return nil -} - -func (st involvedAccountsAnalysisState) evalAccountNamePart(part parser.AccountNamePart) (InvolvedAccountExpr, InterpreterError) { - switch part := part.(type) { - case parser.AccountTextPart: - return AccountLiteral{Account: part.Name}, nil - case *parser.Variable: - expr, ok := st.evaluatedVars[part.Name] - if !ok { - return nil, UnboundVariableErr{Range: part.Range, Name: part.Name} - } - return expr, nil - } - - return nil, InvalidTypeErr{Range: parser.Range{}, Name: fmt.Sprintf("%T", part)} -} - -// Constant folding for the Add{} node. -func foldedAdd(left InvolvedAccountExpr, right InvolvedAccountExpr) InvolvedAccountExpr { - switch left.(type) { - // TODO(impl) bonus: implement folds - // note that it's correct even without constant folding - - default: - return Add{Left: left, Right: right} - } -} - -// Constant folding for the Concat{} node. -func foldedConcatAccount(left InvolvedAccountExpr, right InvolvedAccountExpr) InvolvedAccountExpr { - // For the sake of simplicity, this still doesn't stringify number literal - - leftLit, okLeft := left.(AccountLiteral) - rightLit, okRight := right.(AccountLiteral) - if !okLeft || !okRight { - return ConcatAccount{Left: left, Right: right} - } - - return AccountLiteral{ - Account: leftLit.Account + rightLit.Account, - } -} - -func foldedGetAsset(expr InvolvedAccountExpr) InvolvedAccountExpr { - switch expr := expr.(type) { - case MakeMonetary: - return expr.Asset - - default: - return GetAsset{Monetary: expr} - } -} - -func foldedGetAmount(expr InvolvedAccountExpr) InvolvedAccountExpr { - switch expr := expr.(type) { - case MakeMonetary: - return expr.Amount - - default: - return GetAmount{Monetary: expr} - } -} - -func (st *involvedAccountsAnalysisState) evalVar(expr parser.ValueExpr, typ string) (InvolvedAccountExpr, InterpreterError) { - switch expr := expr.(type) { - case *parser.FnCall: - switch expr.Caller.Name { - case analysis.FnVarOriginMeta: - if len(expr.Args) != 2 { - return nil, BadArityErr{Range: expr.Range, ExpectedArity: 2, GivenArguments: len(expr.Args)} - } - - acc, err := st.evalExpr(expr.Args[0]) - if err != nil { - return nil, err - } - key, err := st.evalExpr(expr.Args[1]) - if err != nil { - return nil, err - } - - st.involvedMeta = append(st.involvedMeta, InvolvedMeta{ - Account: acc, - Key: key, - Write: nil, - }) - - return FnMeta{ - ExpectedType: typ, - Account: acc, - Key: key, - }, nil - } - } - - return st.evalExpr(expr) -} - -func (st *involvedAccountsAnalysisState) evalExpr(expr parser.ValueExpr) (InvolvedAccountExpr, InterpreterError) { - switch expr := expr.(type) { - case *parser.AccountInterpLiteral: - var acc InvolvedAccountExpr - for _, part := range expr.Parts { - partExpr, err := st.evalAccountNamePart(part) - if err != nil { - return nil, err - } - if acc == nil { - acc = partExpr - } else { - acc = foldedConcatAccount(acc, partExpr) - } - } - return acc, nil - - case *parser.AssetLiteral: - return AssetLiteral{Asset: expr.Asset}, nil - - case *parser.Variable: - varLookup, ok := st.evaluatedVars[expr.Name] - if !ok { - return nil, UnboundVariableErr{Range: expr.Range, Name: expr.Name} - } - return varLookup, nil - - case *parser.NumberLiteral: - return NumberLiteral{Amount: expr.Number}, nil - - case *parser.MonetaryLiteral: - evalAmt, err := st.evalExpr(expr.Amount) - if err != nil { - return nil, err - } - evalAsset, err := st.evalExpr(expr.Asset) - if err != nil { - return nil, err - } - return MakeMonetary{Amount: evalAmt, Asset: evalAsset}, nil - - case *parser.StringLiteral: - return StringLiteral{String: expr.String}, nil - - case *parser.Prefix: - inner, err := st.evalExpr(expr.Expr) - if err != nil { - return nil, err - } - switch expr.Operator { - case parser.PrefixOperatorMinus: - return SubPrefix{Expr: inner}, nil - default: - return nil, InvalidOperatorErr{Range: expr.Range, Operator: string(expr.Operator)} - } - - case *parser.BinaryInfix: - evalLeft, err := st.evalExpr(expr.Left) - if err != nil { - return nil, err - } - evalRight, err := st.evalExpr(expr.Right) - if err != nil { - return nil, err - } - switch expr.Operator { - case parser.InfixOperatorMinus: - return Sub{Left: evalLeft, Right: evalRight}, nil - case parser.InfixOperatorDiv: - return Div{Left: evalLeft, Right: evalRight}, nil - case parser.InfixOperatorPlus: - return foldedAdd(evalLeft, evalRight), nil - default: - return nil, InvalidOperatorErr{Range: expr.Range, Operator: string(expr.Operator)} - } - - case *parser.PercentageLiteral: - rat := expr.ToRatio() - return Div{ - Left: NumberLiteral{rat.Num()}, - Right: NumberLiteral{rat.Denom()}, - }, nil - - case *parser.FnCall: - switch expr.Caller.Name { - case analysis.FnVarOriginMeta: - return nil, InvalidNestedMeta{Range: expr.Range} - - case analysis.FnVarOriginOverdraft: - if len(expr.Args) != 2 { - return nil, BadArityErr{Range: expr.Range, ExpectedArity: 2, GivenArguments: len(expr.Args)} - } - acc, err := st.evalExpr(expr.Args[0]) - if err != nil { - return nil, err - } - asset, err := st.evalExpr(expr.Args[1]) - if err != nil { - return nil, err - } - st.involvedAccounts = append(st.involvedAccounts, InvolvedAccount{ - AccountExpr: acc, - AssetExpr: asset, - }) - return GetOverdraft{ - Account: acc, - Asset: asset, - }, nil - - case analysis.FnVarOriginBalance: - if len(expr.Args) != 2 { - return nil, BadArityErr{Range: expr.Range, ExpectedArity: 2, GivenArguments: len(expr.Args)} - } - acc, err := st.evalExpr(expr.Args[0]) - if err != nil { - return nil, err - } - asset, err := st.evalExpr(expr.Args[1]) - if err != nil { - return nil, err - } - st.involvedAccounts = append(st.involvedAccounts, InvolvedAccount{ - AccountExpr: acc, - AssetExpr: asset, - }) - return GetBalance{ - Account: acc, - Asset: asset, - }, nil - - case analysis.FnVarOriginGetAmount: - if len(expr.Args) != 1 { - return nil, BadArityErr{Range: expr.Range, ExpectedArity: 1, GivenArguments: len(expr.Args)} - } - monetary, err := st.evalExpr(expr.Args[0]) - if err != nil { - return nil, err - } - return foldedGetAmount(monetary), nil - - case analysis.FnVarOriginGetAsset: - if len(expr.Args) != 1 { - return nil, BadArityErr{Range: expr.Range, ExpectedArity: 1, GivenArguments: len(expr.Args)} - } - monetary, err := st.evalExpr(expr.Args[0]) - if err != nil { - return nil, err - } - return foldedGetAsset(monetary), nil - - default: - return nil, UnboundFunctionErr{Range: expr.Range, Name: expr.Caller.Name} - } - - default: - return nil, InvalidTypeErr{Range: expr.GetRange(), Name: fmt.Sprintf("%T", expr)} - } -} - -func (st *involvedAccountsAnalysisState) evalSrc(source parser.Source) InterpreterError { - switch source := source.(type) { - case *parser.SourceWithScaling: - return ExperimentalFeature{FlagName: flags.AssetScaling} - - case *parser.SourceOverdraft: - if source.Color != nil { - return ExperimentalFeature{FlagName: flags.AssetScaling} - } - - accountExpr, err := st.evalExpr(source.Address) - if err != nil { - return err - } - st.involvedAccounts = append(st.involvedAccounts, InvolvedAccount{ - AccountExpr: accountExpr, - AssetExpr: st.currentAsset, - }) - - case *parser.SourceAccount: - if source.Color != nil { - return ExperimentalFeature{FlagName: flags.AssetScaling} - } - - accountExpr, err := st.evalExpr(source.ValueExpr) - if err != nil { - return err - } - st.involvedAccounts = append(st.involvedAccounts, InvolvedAccount{ - AccountExpr: accountExpr, - AssetExpr: st.currentAsset, - }) - - case *parser.SourceInorder: - for _, acc := range source.Sources { - if err := st.evalSrc(acc); err != nil { - return err - } - } - - case *parser.SourceOneof: - for _, acc := range source.Sources { - if err := st.evalSrc(acc); err != nil { - return err - } - } - - case *parser.SourceCapped: - return st.evalSrc(source.From) - - case *parser.SourceAllotment: - for _, acc := range source.Items { - if err := st.evalSrc(acc.From); err != nil { - return err - } - } - } - return nil -} - -func (st *involvedAccountsAnalysisState) evalDest(dest parser.Destination) InterpreterError { - switch dest := dest.(type) { - case *parser.DestinationAccount: - accountExpr, err := st.evalExpr(dest.ValueExpr) - if err != nil { - return err - } - st.involvedAccounts = append(st.involvedAccounts, InvolvedAccount{ - AccountExpr: accountExpr, - AssetExpr: st.currentAsset, - }) - - case *parser.DestinationInorder: - for _, clause := range dest.Clauses { - if err := st.evalKeptOrDest(clause.To); err != nil { - return err - } - } - if err := st.evalKeptOrDest(dest.Remaining); err != nil { - return err - } - - case *parser.DestinationOneof: - for _, acc := range dest.Clauses { - if err := st.evalKeptOrDest(acc.To); err != nil { - return err - } - } - if err := st.evalKeptOrDest(dest.Remaining); err != nil { - return err - } - - case *parser.DestinationAllotment: - for _, acc := range dest.Items { - if err := st.evalKeptOrDest(acc.To); err != nil { - return err - } - } - } - return nil -} - -func (st *involvedAccountsAnalysisState) evalKeptOrDest(keptOrDest parser.KeptOrDestination) InterpreterError { - switch keptOrDest := keptOrDest.(type) { - case *parser.DestinationKept: - // nothing to do here - case *parser.DestinationTo: - return st.evalDest(keptOrDest.Destination) - } - return nil -} - -type isValidCallState struct { - isTopLevel bool -} - -func (st *isValidCallState) isValidCall(expr InvolvedAccountExpr) bool { - isTopLevel := st.isTopLevel - st.isTopLevel = false - - switch expr := expr.(type) { - case GetBalance: - return isTopLevel - - case NumberLiteral, StringLiteral, AssetLiteral, AccountLiteral: - return true - - case ConcatAccount: - return st.isValidCall(expr.Left) && st.isValidCall(expr.Right) - case Add: - return st.isValidCall(expr.Left) && st.isValidCall(expr.Right) - case Sub: - return st.isValidCall(expr.Left) && st.isValidCall(expr.Right) - case Div: - return st.isValidCall(expr.Left) && st.isValidCall(expr.Right) - case SubPrefix: - return st.isValidCall(expr.Expr) - case GetAmount: - return st.isValidCall(expr.Monetary) - case GetAsset: - return st.isValidCall(expr.Monetary) - - case MakeMonetary: - return st.isValidCall(expr.Amount) && st.isValidCall(expr.Asset) - - case FnMeta: - return st.isValidCall(expr.Account) && st.isValidCall(expr.Key) - } - - return false -} - -func IsValidCall(expr InvolvedAccountExpr) bool { - st := isValidCallState{isTopLevel: true} - return st.isValidCall(expr) -} diff --git a/internal/interpreter/get_involved_accounts_test.go b/internal/interpreter/get_involved_accounts_test.go deleted file mode 100644 index 214d15e8..00000000 --- a/internal/interpreter/get_involved_accounts_test.go +++ /dev/null @@ -1,559 +0,0 @@ -package interpreter_test - -import ( - "testing" - - "github.com/formancehq/numscript/internal/interpreter" - "github.com/formancehq/numscript/internal/parser" - "github.com/gkampitakis/go-snaps/snaps" - "github.com/stretchr/testify/require" -) - -func getInvolvedAccounts(t *testing.T, vars interpreter.VariablesMap, src string) ([]interpreter.InvolvedAccount, []interpreter.InvolvedMeta) { - t.Helper() - out := parser.Parse(src) - require.Empty(t, out.Errors) - accs, meta, err := interpreter.GetInvolvedAccounts(vars, out.Value) - require.NoError(t, err) - return accs, meta -} - -func getInvolvedAccountsErr(t *testing.T, vars interpreter.VariablesMap, src string) interpreter.InterpreterError { - t.Helper() - out := parser.Parse(src) - _, _, err := interpreter.GetInvolvedAccounts(vars, out.Value) - require.Error(t, err) - return err -} - -func TestGetInvolvedAccount(t *testing.T) { - t.Run("simple (no vars)", func(t *testing.T) { - accs, meta := getInvolvedAccounts(t, interpreter.VariablesMap{}, ` - send [USD/2 *] ( - source = @src - destination = @dest - ) - `) - require.Nil(t, meta) - snaps.MatchInlineSnapshot(t, accs, snaps.Inline(`[]interpreter.InvolvedAccount{ - { - AccountExpr: interpreter.AccountLiteral{Account:"src"}, - AssetExpr: interpreter.AssetLiteral{Asset:"USD/2"}, - }, - { - AccountExpr: interpreter.AccountLiteral{Account:"dest"}, - AssetExpr: interpreter.AssetLiteral{Asset:"USD/2"}, - }, -}`)) - }) - - t.Run("simple (get var)", func(t *testing.T) { - accs, meta := getInvolvedAccounts(t, interpreter.VariablesMap{ - "acc": "acc_value_after_subst", - }, ` - vars { account $acc } - send [USD/2 *] ( - source = $acc - destination = @dest - ) - `) - require.Nil(t, meta) - snaps.MatchInlineSnapshot(t, accs, snaps.Inline(`[]interpreter.InvolvedAccount{ - { - AccountExpr: interpreter.AccountLiteral{Account:"acc_value_after_subst"}, - AssetExpr: interpreter.AssetLiteral{Asset:"USD/2"}, - }, - { - AccountExpr: interpreter.AccountLiteral{Account:"dest"}, - AssetExpr: interpreter.AssetLiteral{Asset:"USD/2"}, - }, -}`)) - }) - - t.Run("simple (account interp var)", func(t *testing.T) { - accs, meta := getInvolvedAccounts(t, interpreter.VariablesMap{ - "id": "42", - }, ` - vars { number $id } - send [USD/2 *] ( - source = @user:$id:pending - destination = @dest - ) - `) - require.Nil(t, meta) - snaps.MatchInlineSnapshot(t, accs, snaps.Inline(`[]interpreter.InvolvedAccount{ - { - AccountExpr: interpreter.ConcatAccount{ - Left: interpreter.ConcatAccount{ - Left: interpreter.ConcatAccount{ - Left: interpreter.AccountLiteral{Account:"user:"}, - Right: interpreter.NumberLiteral{ - Amount: &big.Int{ - neg: false, - abs: {0x2a}, - }, - }, - }, - Right: interpreter.AccountLiteral{Account:":"}, - }, - Right: interpreter.AccountLiteral{Account:"pending"}, - }, - AssetExpr: interpreter.AssetLiteral{Asset:"USD/2"}, - }, - { - AccountExpr: interpreter.AccountLiteral{Account:"dest"}, - AssetExpr: interpreter.AssetLiteral{Asset:"USD/2"}, - }, -}`), - ) - }) - - t.Run("eval var expr", func(t *testing.T) { - accs, meta := getInvolvedAccounts(t, interpreter.VariablesMap{}, ` - vars { account $acc = [EUR/2 (100 + 42)] } - send [USD/2 *] ( - source = $acc - destination = @dest - ) - `) - require.Nil(t, meta) - snaps.MatchInlineSnapshot(t, accs, snaps.Inline(`[]interpreter.InvolvedAccount{ - { - AccountExpr: interpreter.MakeMonetary{ - Asset: interpreter.AssetLiteral{Asset:"EUR/2"}, - Amount: interpreter.Add{ - Left: interpreter.NumberLiteral{ - Amount: &big.Int{ - neg: false, - abs: {0x64}, - }, - }, - Right: interpreter.NumberLiteral{ - Amount: &big.Int{ - neg: false, - abs: {0x2a}, - }, - }, - }, - }, - AssetExpr: interpreter.AssetLiteral{Asset:"USD/2"}, - }, - { - AccountExpr: interpreter.AccountLiteral{Account:"dest"}, - AssetExpr: interpreter.AssetLiteral{Asset:"USD/2"}, - }, -}`)) - }) - - t.Run("required meta", func(t *testing.T) { - accs, meta := getInvolvedAccounts(t, interpreter.VariablesMap{}, ` - // this does not involve accounts, but it's required meta - vars { account $acc = meta(@acc, "k") } - `) - snaps.MatchInlineSnapshot(t, meta, snaps.Inline(`[]interpreter.InvolvedMeta{ - { - Account: interpreter.AccountLiteral{Account:"acc"}, - Key: interpreter.StringLiteral{String:"k"}, - Write: nil, - }, -}`)) - snaps.MatchInlineSnapshot(t, accs, snaps.Inline("[]interpreter.InvolvedAccount(nil)")) - }) - - t.Run("required meta (write)", func(t *testing.T) { - accs, meta := getInvolvedAccounts(t, interpreter.VariablesMap{}, ` - set_account_meta(@acc, "k", 42) - `) - snaps.MatchInlineSnapshot(t, meta, snaps.Inline(`[]interpreter.InvolvedMeta{ - { - Account: interpreter.AccountLiteral{Account:"acc"}, - Key: interpreter.StringLiteral{String:"k"}, - Write: interpreter.NumberLiteral{ - Amount: &big.Int{ - neg: false, - abs: {0x2a}, - }, - }, - }, -}`)) - snaps.MatchInlineSnapshot(t, accs, snaps.Inline("[]interpreter.InvolvedAccount(nil)")) - }) - - t.Run("meta fn check", func(t *testing.T) { - accs, meta := getInvolvedAccounts(t, interpreter.VariablesMap{}, ` - vars { account $acc = meta(@acc, "k") } - send [USD/2 *] ( - source = $acc - destination = @dest - ) - `) - snaps.MatchInlineSnapshot(t, meta, snaps.Inline(`[]interpreter.InvolvedMeta{ - { - Account: interpreter.AccountLiteral{Account:"acc"}, - Key: interpreter.StringLiteral{String:"k"}, - Write: nil, - }, -}`)) - snaps.MatchInlineSnapshot(t, accs, snaps.Inline(`[]interpreter.InvolvedAccount{ - { - AccountExpr: interpreter.FnMeta{ - ExpectedType: "account", - Account: interpreter.AccountLiteral{Account:"acc"}, - Key: interpreter.StringLiteral{String:"k"}, - }, - AssetExpr: interpreter.AssetLiteral{Asset:"USD/2"}, - }, - { - AccountExpr: interpreter.AccountLiteral{Account:"dest"}, - AssetExpr: interpreter.AssetLiteral{Asset:"USD/2"}, - }, -}`)) - }) - - t.Run("unresolved meta under string addition", func(t *testing.T) { - accs, _ := getInvolvedAccounts(t, interpreter.VariablesMap{}, ` - vars { account $acc = meta(@acc, "k") } - send [USD/2 *] ( - source = @user:$acc:pending - destination = @dest - ) - `) - - snaps.MatchInlineSnapshot(t, accs, snaps.Inline(`[]interpreter.InvolvedAccount{ - { - AccountExpr: interpreter.ConcatAccount{ - Left: interpreter.ConcatAccount{ - Left: interpreter.ConcatAccount{ - Left: interpreter.AccountLiteral{Account:"user:"}, - Right: interpreter.FnMeta{ - ExpectedType: "account", - Account: interpreter.AccountLiteral{Account:"acc"}, - Key: interpreter.StringLiteral{String:"k"}, - }, - }, - Right: interpreter.AccountLiteral{Account:":"}, - }, - Right: interpreter.AccountLiteral{Account:"pending"}, - }, - AssetExpr: interpreter.AssetLiteral{Asset:"USD/2"}, - }, - { - AccountExpr: interpreter.AccountLiteral{Account:"dest"}, - AssetExpr: interpreter.AssetLiteral{Asset:"USD/2"}, - }, -}`), - ) - }) - - t.Run("nested meta fn check", func(t *testing.T) { - accs, meta := getInvolvedAccounts(t, interpreter.VariablesMap{}, ` - vars { - string $s = meta(@a1, "k") - account $acc = meta(@acc, $s) - } - - send [USD/2 *] ( - source = $acc - destination = @dest - ) - `) - snaps.MatchInlineSnapshot(t, meta, snaps.Inline(`[]interpreter.InvolvedMeta{ - { - Account: interpreter.AccountLiteral{Account:"a1"}, - Key: interpreter.StringLiteral{String:"k"}, - Write: nil, - }, - { - Account: interpreter.AccountLiteral{Account:"acc"}, - Key: interpreter.FnMeta{ - ExpectedType: "string", - Account: interpreter.AccountLiteral{Account:"a1"}, - Key: interpreter.StringLiteral{String:"k"}, - }, - Write: nil, - }, -}`)) - snaps.MatchInlineSnapshot(t, accs, snaps.Inline(`[]interpreter.InvolvedAccount{ - { - AccountExpr: interpreter.FnMeta{ - ExpectedType: "account", - Account: interpreter.AccountLiteral{Account:"acc"}, - Key: interpreter.FnMeta{ - ExpectedType: "string", - Account: interpreter.AccountLiteral{Account:"a1"}, - Key: interpreter.StringLiteral{String:"k"}, - }, - }, - AssetExpr: interpreter.AssetLiteral{Asset:"USD/2"}, - }, - { - AccountExpr: interpreter.AccountLiteral{Account:"dest"}, - AssetExpr: interpreter.AssetLiteral{Asset:"USD/2"}, - }, -}`)) - }) - - t.Run("involved account in balance check", func(t *testing.T) { - accs, meta := getInvolvedAccounts(t, interpreter.VariablesMap{}, ` - vars { - // even if this is dead code, we don't need to care about - // this kind of dead code elimination. It'd make the inference - // harder and we'd have no real world perf gain - // users should just avoid this kind of dead code, and it's marked - // as warning by the "numscript check" command - monetary $acc = balance(@acc, USD/2) - } - `) - require.Nil(t, meta) - snaps.MatchInlineSnapshot(t, accs, snaps.Inline(`[]interpreter.InvolvedAccount{ - { - AccountExpr: interpreter.AccountLiteral{Account:"acc"}, - AssetExpr: interpreter.AssetLiteral{Asset:"USD/2"}, - }, -}`)) - }) - - t.Run("forbid invalid meta keys", func(t *testing.T) { - - accs, meta := getInvolvedAccounts(t, interpreter.VariablesMap{}, ` - vars { - monetary $acc = balance(@acc, USD/2) - number $amt = get_amount($acc) - account $acc = @user:$amt - string $m = meta($acc, "k") - } - `) - snaps.MatchInlineSnapshot(t, meta, snaps.Inline(`[]interpreter.InvolvedMeta{ - { - Account: interpreter.ConcatAccount{ - Left: interpreter.AccountLiteral{Account:"user:"}, - Right: interpreter.GetAmount{ - Monetary: interpreter.GetBalance{ - Account: interpreter.AccountLiteral{Account:"acc"}, - Asset: interpreter.AssetLiteral{Asset:"USD/2"}, - }, - }, - }, - Key: interpreter.StringLiteral{String:"k"}, - Write: nil, - }, -}`)) - - require.False(t, interpreter.IsValidCall(meta[0].Account)) - require.True(t, interpreter.IsValidCall(meta[0].Key)) - snaps.MatchInlineSnapshot(t, accs, snaps.Inline(`[]interpreter.InvolvedAccount{ - { - AccountExpr: interpreter.AccountLiteral{Account:"acc"}, - AssetExpr: interpreter.AssetLiteral{Asset:"USD/2"}, - }, -}`)) - }) - - t.Run("bounded send statements", func(t *testing.T) { - - accs, meta := getInvolvedAccounts(t, interpreter.VariablesMap{}, ` - send [USD/2 100] ( - source = @acc - destination = @dest - ) - `) - snaps.MatchInlineSnapshot(t, meta, snaps.Inline("[]interpreter.InvolvedMeta(nil)")) - snaps.MatchInlineSnapshot(t, accs, snaps.Inline(`[]interpreter.InvolvedAccount{ - { - AccountExpr: interpreter.AccountLiteral{Account:"acc"}, - AssetExpr: interpreter.AssetLiteral{Asset:"USD/2"}, - }, - { - AccountExpr: interpreter.AccountLiteral{Account:"dest"}, - AssetExpr: interpreter.AssetLiteral{Asset:"USD/2"}, - }, -}`)) - }) - - t.Run("eval asset", func(t *testing.T) { - - accs, _ := getInvolvedAccounts(t, interpreter.VariablesMap{}, ` - vars { - asset $a = meta(@acc, "k") - } - send [$a 100] ( - source = @acc - destination = @dest - ) - `) - - snaps.MatchInlineSnapshot(t, accs, snaps.Inline(`[]interpreter.InvolvedAccount{ - { - AccountExpr: interpreter.AccountLiteral{Account:"acc"}, - AssetExpr: interpreter.FnMeta{ - ExpectedType: "asset", - Account: interpreter.AccountLiteral{Account:"acc"}, - Key: interpreter.StringLiteral{String:"k"}, - }, - }, - { - AccountExpr: interpreter.AccountLiteral{Account:"dest"}, - AssetExpr: interpreter.FnMeta{ - ExpectedType: "asset", - Account: interpreter.AccountLiteral{Account:"acc"}, - Key: interpreter.StringLiteral{String:"k"}, - }, - }, -}`)) - - }) - - t.Run("eval asset as meta", func(t *testing.T) { - - accs, _ := getInvolvedAccounts(t, interpreter.VariablesMap{}, ` - vars { - monetary $a = meta(@acc, "k") - } - send $a ( - source = @acc - destination = @dest - ) - `) - - snaps.MatchInlineSnapshot(t, accs, snaps.Inline(`[]interpreter.InvolvedAccount{ - { - AccountExpr: interpreter.AccountLiteral{Account:"acc"}, - AssetExpr: interpreter.GetAsset{ - Monetary: interpreter.FnMeta{ - ExpectedType: "monetary", - Account: interpreter.AccountLiteral{Account:"acc"}, - Key: interpreter.StringLiteral{String:"k"}, - }, - }, - }, - { - AccountExpr: interpreter.AccountLiteral{Account:"dest"}, - AssetExpr: interpreter.GetAsset{ - Monetary: interpreter.FnMeta{ - ExpectedType: "monetary", - Account: interpreter.AccountLiteral{Account:"acc"}, - Key: interpreter.StringLiteral{String:"k"}, - }, - }, - }, -}`)) - - }) - - t.Run("allotment in src and dest", func(t *testing.T) { - - accs, _ := getInvolvedAccounts(t, interpreter.VariablesMap{}, ` - send [USD/2 42] ( - source = { - 1/2 from @a1 - remaining from @a2 - } - destination = { - 1/3 to @d1 - 1/3 kept - remaining to @d2 - } - ) - `) - - snaps.MatchInlineSnapshot(t, accs, snaps.Inline(`[]interpreter.InvolvedAccount{ - { - AccountExpr: interpreter.AccountLiteral{Account:"a1"}, - AssetExpr: interpreter.AssetLiteral{Asset:"USD/2"}, - }, - { - AccountExpr: interpreter.AccountLiteral{Account:"a2"}, - AssetExpr: interpreter.AssetLiteral{Asset:"USD/2"}, - }, - { - AccountExpr: interpreter.AccountLiteral{Account:"d1"}, - AssetExpr: interpreter.AssetLiteral{Asset:"USD/2"}, - }, - { - AccountExpr: interpreter.AccountLiteral{Account:"d2"}, - AssetExpr: interpreter.AssetLiteral{Asset:"USD/2"}, - }, -}`)) - - }) - - t.Run("many assets", func(t *testing.T) { - - accs, _ := getInvolvedAccounts(t, interpreter.VariablesMap{}, ` - send [USD/2 42] ( - source = @a1 - destination = { remaining kept} - ) - send [EUR/2 42] ( - source = @a2 - destination = { remaining kept} - ) - `) - - snaps.MatchInlineSnapshot(t, accs, snaps.Inline(`[]interpreter.InvolvedAccount{ - { - AccountExpr: interpreter.AccountLiteral{Account:"a1"}, - AssetExpr: interpreter.AssetLiteral{Asset:"USD/2"}, - }, - { - AccountExpr: interpreter.AccountLiteral{Account:"a2"}, - AssetExpr: interpreter.AssetLiteral{Asset:"EUR/2"}, - }, -}`)) - - }) - -} - -func TestGetInvolvedAccountsErrors(t *testing.T) { - t.Run("missing variable", func(t *testing.T) { - err := getInvolvedAccountsErr(t, interpreter.VariablesMap{}, ` - vars { account $acc } - send [USD/2 *] ( - source = $acc - destination = @dest - ) - `) - var target interpreter.MissingVariableErr - require.ErrorAs(t, err, &target) - require.Equal(t, "acc", target.Name) - }) - - t.Run("invalid variable value", func(t *testing.T) { - err := getInvolvedAccountsErr(t, interpreter.VariablesMap{"acc": "not a valid account!!"}, ` - vars { account $acc } - send [USD/2 *] ( - source = $acc - destination = @dest - ) - `) - var target interpreter.InvalidAccountName - require.ErrorAs(t, err, &target) - }) - - t.Run("nested meta in var origin args", func(t *testing.T) { - // meta() as an argument to another meta() hits InvalidNestedMeta in evalExpr - err := getInvolvedAccountsErr(t, interpreter.VariablesMap{}, ` - vars { string $x = meta(@a, meta(@b, "k")) } - `) - var target interpreter.InvalidNestedMeta - require.ErrorAs(t, err, &target) - }) - - t.Run("unbound variable in account interpolation", func(t *testing.T) { - // $undeclared is used in source but not declared in vars block; - // parser reports no error (it's syntactically valid), but - // GetInvolvedAccounts returns UnboundVariableErr. - out := parser.Parse(` - send [USD/2 *] ( - source = @user:$undeclared:end - destination = @dest - ) - `) - _, _, err := interpreter.GetInvolvedAccounts(interpreter.VariablesMap{}, out.Value) - require.Error(t, err) - var target interpreter.UnboundVariableErr - require.ErrorAs(t, err, &target) - require.Equal(t, "undeclared", target.Name) - }) -} diff --git a/numscript.go b/numscript.go index 6d11d1c6..8adf703d 100644 --- a/numscript.go +++ b/numscript.go @@ -3,7 +3,6 @@ package numscript import ( "context" - "github.com/formancehq/numscript/accounts" "github.com/formancehq/numscript/internal/interpreter" "github.com/formancehq/numscript/internal/parser" ) @@ -106,7 +105,3 @@ func (p ParseResult) RunWithFeatureFlags( func (p ParseResult) GetSource() string { return p.parseResult.Source } - -func (p ParseResult) GetInvolvedAccounts(vars VariablesMap) ([]accounts.InvolvedAccount, []accounts.InvolvedMeta, InterpreterError) { - return interpreter.GetInvolvedAccounts(vars, p.parseResult.Value) -}