diff --git a/accounts/accounts.go b/accounts/accounts.go new file mode 100644 index 00000000..c99b6f34 --- /dev/null +++ b/accounts/accounts.go @@ -0,0 +1,27 @@ +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/analysis/check.go b/internal/analysis/check.go index c0baa4d9..9b126fef 100644 --- a/internal/analysis/check.go +++ b/internal/analysis/check.go @@ -331,7 +331,7 @@ func (res *CheckResult) checkStatement(statement parser.Statement) { switch statement := statement.(type) { case *parser.SaveStatement: res.checkSentValue(statement.SentValue) - res.checkExpression(statement.Amount, TypeAccount) + res.checkExpression(statement.Account, TypeAccount) case *parser.SendStatement: _, isSendAll := statement.SentValue.(*parser.SentValueAll) diff --git a/internal/analysis/hover.go b/internal/analysis/hover.go index 3b7bb626..607e5bff 100644 --- a/internal/analysis/hover.go +++ b/internal/analysis/hover.go @@ -110,7 +110,7 @@ func hoverOnSaveStatement(saveStatement parser.SaveStatement, position parser.Po return hover } - hover = hoverOnExpression(saveStatement.Amount, position) + hover = hoverOnExpression(saveStatement.Account, position) if hover != nil { return hover } diff --git a/internal/interpreter/batch_balances_query.go b/internal/interpreter/batch_balances_query.go index 6d95df4a..141d17ab 100644 --- a/internal/interpreter/batch_balances_query.go +++ b/internal/interpreter/batch_balances_query.go @@ -27,7 +27,7 @@ func (st *programState) findBalancesQueriesInStatement(statement parser.Statemen // // this would mean that the "save" statement was not needed in the first place, // so preventing this query would hardly be an useful optimization - account, err := evaluateExprAs(st, statement.Amount, expectAccount) + account, err := evaluateExprAs(st, statement.Account, expectAccount) if err != nil { return err } diff --git a/internal/interpreter/get_involved_accounts.go b/internal/interpreter/get_involved_accounts.go new file mode 100644 index 00000000..d0aa392e --- /dev/null +++ b/internal/interpreter/get_involved_accounts.go @@ -0,0 +1,689 @@ +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 new file mode 100644 index 00000000..214d15e8 --- /dev/null +++ b/internal/interpreter/get_involved_accounts_test.go @@ -0,0 +1,559 @@ +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/internal/interpreter/interpreter.go b/internal/interpreter/interpreter.go index 668890c7..94b634af 100644 --- a/internal/interpreter/interpreter.go +++ b/internal/interpreter/interpreter.go @@ -485,7 +485,7 @@ func (st *programState) runSaveStatement(saveStatement parser.SaveStatement) Int return err } - account, err := evaluateExprAs(st, saveStatement.Amount, expectAccount) + account, err := evaluateExprAs(st, saveStatement.Account, expectAccount) if err != nil { return err } diff --git a/internal/interpreter/interpreter_error.go b/internal/interpreter/interpreter_error.go index b329e6b8..3d8527c3 100644 --- a/internal/interpreter/interpreter_error.go +++ b/internal/interpreter/interpreter_error.go @@ -277,3 +277,12 @@ type InvalidFeature struct { func (e InvalidFeature) Error() string { return fmt.Sprintf("Invalid feature: %s", e.Feature) } + +type InvalidOperatorErr struct { + parser.Range + Operator string +} + +func (e InvalidOperatorErr) Error() string { + return fmt.Sprintf("Invalid operator: %s", e.Operator) +} diff --git a/internal/parser/__snapshots__/parser_fault_tolerance_test.snap b/internal/parser/__snapshots__/parser_fault_tolerance_test.snap index 62d06080..bc47d8b4 100755 --- a/internal/parser/__snapshots__/parser_fault_tolerance_test.snap +++ b/internal/parser/__snapshots__/parser_fault_tolerance_test.snap @@ -133,6 +133,10 @@ parser.Program{ }, }, Source: &parser.SourceAccount{ + Range: parser.Range{ + Start: parser.Position{Character:13, Line:1}, + End: parser.Position{Character:15, Line:1}, + }, Color: nil, ValueExpr: &parser.AccountInterpLiteral{ Range: parser.Range{ @@ -291,6 +295,10 @@ parser.Program{ }, }, Source: &parser.SourceAccount{ + Range: parser.Range{ + Start: parser.Position{Character:11, Line:1}, + End: parser.Position{Character:13, Line:1}, + }, Color: nil, ValueExpr: &parser.AccountInterpLiteral{ Range: parser.Range{ @@ -395,7 +403,7 @@ parser.Program{ End: parser.Position{Character:4, Line:1}, }, SentValue: nil, - Amount: nil, + Account: nil, }, }, Comments: nil, @@ -441,7 +449,7 @@ parser.Program{ }, }, }, - Amount: nil, + Account: nil, }, }, Comments: nil, @@ -487,7 +495,7 @@ parser.Program{ }, }, }, - Amount: nil, + Account: nil, }, }, Comments: nil, diff --git a/internal/parser/__snapshots__/parser_test.snap b/internal/parser/__snapshots__/parser_test.snap index fe7ac480..79a02c30 100755 --- a/internal/parser/__snapshots__/parser_test.snap +++ b/internal/parser/__snapshots__/parser_test.snap @@ -38,6 +38,10 @@ parser.Program{ }, }, Source: &parser.SourceAccount{ + Range: parser.Range{ + Start: parser.Position{Character:11, Line:1}, + End: parser.Position{Character:15, Line:1}, + }, Color: nil, ValueExpr: &parser.AccountInterpLiteral{ Range: parser.Range{ @@ -106,6 +110,10 @@ parser.Program{ }, }, Source: &parser.SourceAccount{ + Range: parser.Range{ + Start: parser.Position{Character:11, Line:1}, + End: parser.Position{Character:27, Line:1}, + }, Color: nil, ValueExpr: &parser.Variable{ Range: parser.Range{ @@ -210,6 +218,10 @@ parser.Program{ }, }, From: &parser.SourceAccount{ + Range: parser.Range{ + Start: parser.Position{Character:22, Line:1}, + End: parser.Position{Character:25, Line:1}, + }, Color: nil, ValueExpr: &parser.AccountInterpLiteral{ Range: parser.Range{ @@ -305,6 +317,10 @@ parser.Program{ }, }, From: &parser.SourceAccount{ + Range: parser.Range{ + Start: parser.Position{Character:13, Line:2}, + End: parser.Position{Character:16, Line:2}, + }, Color: nil, ValueExpr: &parser.AccountInterpLiteral{ Range: parser.Range{ @@ -352,6 +368,10 @@ parser.Program{ }, }, From: &parser.SourceAccount{ + Range: parser.Range{ + Start: parser.Position{Character:10, Line:3}, + End: parser.Position{Character:13, Line:3}, + }, Color: nil, ValueExpr: &parser.AccountInterpLiteral{ Range: parser.Range{ @@ -376,6 +396,10 @@ parser.Program{ }, }, From: &parser.SourceAccount{ + Range: parser.Range{ + Start: parser.Position{Character:16, Line:4}, + End: parser.Position{Character:19, Line:4}, + }, Color: nil, ValueExpr: &parser.AccountInterpLiteral{ Range: parser.Range{ @@ -471,6 +495,10 @@ parser.Program{ }, }, From: &parser.SourceAccount{ + Range: parser.Range{ + Start: parser.Position{Character:24, Line:1}, + End: parser.Position{Character:26, Line:1}, + }, Color: nil, ValueExpr: &parser.AccountInterpLiteral{ Range: parser.Range{ @@ -542,6 +570,10 @@ parser.Program{ }, }, Source: &parser.SourceAccount{ + Range: parser.Range{ + Start: parser.Position{Character:11, Line:1}, + End: parser.Position{Character:13, Line:1}, + }, Color: nil, ValueExpr: &parser.AccountInterpLiteral{ Range: parser.Range{ @@ -660,6 +692,10 @@ parser.Program{ End: parser.Position{Character:35, Line:1}, }, From: &parser.SourceAccount{ + Range: parser.Range{ + Start: parser.Position{Character:31, Line:1}, + End: parser.Position{Character:35, Line:1}, + }, Color: nil, ValueExpr: &parser.AccountInterpLiteral{ Range: parser.Range{ @@ -757,6 +793,10 @@ parser.Program{ End: parser.Position{Character:32, Line:1}, }, From: &parser.SourceAccount{ + Range: parser.Range{ + Start: parser.Position{Character:28, Line:1}, + End: parser.Position{Character:32, Line:1}, + }, Color: nil, ValueExpr: &parser.AccountInterpLiteral{ Range: parser.Range{ @@ -844,6 +884,10 @@ parser.Program{ End: parser.Position{Character:27, Line:2}, }, From: &parser.SourceAccount{ + Range: parser.Range{ + Start: parser.Position{Character:23, Line:2}, + End: parser.Position{Character:27, Line:2}, + }, Color: nil, ValueExpr: &parser.AccountInterpLiteral{ Range: parser.Range{ @@ -880,6 +924,10 @@ parser.Program{ }, }, &parser.SourceAccount{ + Range: parser.Range{ + Start: parser.Position{Character:1, Line:3}, + End: parser.Position{Character:3, Line:3}, + }, Color: nil, ValueExpr: &parser.AccountInterpLiteral{ Range: parser.Range{ @@ -892,6 +940,10 @@ parser.Program{ }, }, &parser.SourceAccount{ + Range: parser.Range{ + Start: parser.Position{Character:1, Line:4}, + End: parser.Position{Character:3, Line:4}, + }, Color: nil, ValueExpr: &parser.AccountInterpLiteral{ Range: parser.Range{ @@ -962,6 +1014,10 @@ parser.Program{ }, }, Source: &parser.SourceAccount{ + Range: parser.Range{ + Start: parser.Position{Character:27, Line:1}, + End: parser.Position{Character:31, Line:1}, + }, Color: nil, ValueExpr: &parser.AccountInterpLiteral{ Range: parser.Range{ @@ -1020,6 +1076,10 @@ parser.Program{ }, }, Source: &parser.SourceAccount{ + Range: parser.Range{ + Start: parser.Position{Character:27, Line:2}, + End: parser.Position{Character:31, Line:2}, + }, Color: nil, ValueExpr: &parser.AccountInterpLiteral{ Range: parser.Range{ @@ -1123,6 +1183,10 @@ parser.Program{ }, }, Source: &parser.SourceAccount{ + Range: parser.Range{ + Start: parser.Position{Character:11, Line:1}, + End: parser.Position{Character:13, Line:1}, + }, Color: nil, ValueExpr: &parser.AccountInterpLiteral{ Range: parser.Range{ @@ -1191,6 +1255,10 @@ parser.Program{ }, }, Source: &parser.SourceAccount{ + Range: parser.Range{ + Start: parser.Position{Character:11, Line:1}, + End: parser.Position{Character:13, Line:1}, + }, Color: nil, ValueExpr: &parser.AccountInterpLiteral{ Range: parser.Range{ @@ -1349,6 +1417,10 @@ parser.Program{ }, }, From: &parser.SourceAccount{ + Range: parser.Range{ + Start: parser.Position{Character:9, Line:2}, + End: parser.Position{Character:11, Line:2}, + }, Color: nil, ValueExpr: &parser.AccountInterpLiteral{ Range: parser.Range{ @@ -1839,6 +1911,10 @@ parser.Program{ }, Sources: { &parser.SourceAccount{ + Range: parser.Range{ + Start: parser.Position{Character:13, Line:1}, + End: parser.Position{Character:16, Line:1}, + }, Color: nil, ValueExpr: &parser.AccountInterpLiteral{ Range: parser.Range{ @@ -1851,6 +1927,10 @@ parser.Program{ }, }, &parser.SourceAccount{ + Range: parser.Range{ + Start: parser.Position{Character:17, Line:1}, + End: parser.Position{Character:20, Line:1}, + }, Color: nil, ValueExpr: &parser.AccountInterpLiteral{ Range: parser.Range{ @@ -1905,6 +1985,10 @@ parser.Program{ }, }, Source: &parser.SourceAccount{ + Range: parser.Range{ + Start: parser.Position{Character:11, Line:1}, + End: parser.Position{Character:13, Line:1}, + }, Color: nil, ValueExpr: &parser.AccountInterpLiteral{ Range: parser.Range{ @@ -2028,6 +2112,10 @@ parser.Program{ }, }, Source: &parser.SourceAccount{ + Range: parser.Range{ + Start: parser.Position{Character:10, Line:1}, + End: parser.Position{Character:12, Line:1}, + }, Color: nil, ValueExpr: &parser.AccountInterpLiteral{ Range: parser.Range{ @@ -2096,6 +2184,10 @@ parser.Program{ }, }, Source: &parser.SourceAccount{ + Range: parser.Range{ + Start: parser.Position{Character:11, Line:1}, + End: parser.Position{Character:13, Line:1}, + }, Color: nil, ValueExpr: &parser.AccountInterpLiteral{ Range: parser.Range{ @@ -2186,6 +2278,10 @@ parser.Program{ }, }, Source: &parser.SourceAccount{ + Range: parser.Range{ + Start: parser.Position{Character:11, Line:2}, + End: parser.Position{Character:17, Line:2}, + }, Color: nil, ValueExpr: &parser.AccountInterpLiteral{ Range: parser.Range{ @@ -2327,6 +2423,10 @@ parser.Program{ }, }, Source: &parser.SourceAccount{ + Range: parser.Range{ + Start: parser.Position{Character:11, Line:1}, + End: parser.Position{Character:15, Line:1}, + }, Color: nil, ValueExpr: &parser.AccountInterpLiteral{ Range: parser.Range{ @@ -2394,7 +2494,7 @@ parser.Program{ }, }, }, - Amount: &parser.AccountInterpLiteral{ + Account: &parser.AccountInterpLiteral{ Range: parser.Range{ Start: parser.Position{Character:22, Line:1}, End: parser.Position{Character:28, Line:1}, @@ -2432,7 +2532,7 @@ parser.Program{ Asset: "EUR/2", }, }, - Amount: &parser.AccountInterpLiteral{ + Account: &parser.AccountInterpLiteral{ Range: parser.Range{ Start: parser.Position{Character:20, Line:1}, End: parser.Position{Character:26, Line:1}, @@ -2470,7 +2570,7 @@ parser.Program{ Name: "amt", }, }, - Amount: &parser.Variable{ + Account: &parser.Variable{ Range: parser.Range{ Start: parser.Position{Character:15, Line:1}, End: parser.Position{Character:19, Line:1}, @@ -2843,6 +2943,10 @@ parser.Program{ }, Sources: { &parser.SourceAccount{ + Range: parser.Range{ + Start: parser.Position{Character:19, Line:1}, + End: parser.Position{Character:22, Line:1}, + }, Color: nil, ValueExpr: &parser.AccountInterpLiteral{ Range: parser.Range{ @@ -2855,6 +2959,10 @@ parser.Program{ }, }, &parser.SourceAccount{ + Range: parser.Range{ + Start: parser.Position{Character:23, Line:1}, + End: parser.Position{Character:26, Line:1}, + }, Color: nil, ValueExpr: &parser.AccountInterpLiteral{ Range: parser.Range{ @@ -2909,6 +3017,10 @@ parser.Program{ }, }, Source: &parser.SourceAccount{ + Range: parser.Range{ + Start: parser.Position{Character:10, Line:1}, + End: parser.Position{Character:12, Line:1}, + }, Color: nil, ValueExpr: &parser.AccountInterpLiteral{ Range: parser.Range{ @@ -3466,6 +3578,10 @@ parser.Program{ }, }, Source: &parser.SourceAccount{ + Range: parser.Range{ + Start: parser.Position{Character:11, Line:1}, + End: parser.Position{Character:21, Line:1}, + }, Color: &parser.StringLiteral{ Range: parser.Range{ Start: parser.Position{Character:16, Line:1}, diff --git a/internal/parser/ast.go b/internal/parser/ast.go index 7ced1db5..eb09c19b 100644 --- a/internal/parser/ast.go +++ b/internal/parser/ast.go @@ -147,8 +147,9 @@ func (*SourceWithScaling) source() {} type ( SourceAccount struct { - Color ValueExpr - ValueExpr + Range + Color ValueExpr + ValueExpr ValueExpr } SourceInorder struct { @@ -318,7 +319,7 @@ type SendStatement struct { type SaveStatement struct { Range SentValue SentValue - Amount ValueExpr + Account ValueExpr } type TypeDecl struct { diff --git a/internal/parser/parser.go b/internal/parser/parser.go index 0ebbb470..5068bd2e 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -194,6 +194,7 @@ func parseSource(sourceCtx antlrParser.ISourceContext) Source { switch sourceCtx := sourceCtx.(type) { case *antlrParser.SrcAccountContext: return &SourceAccount{ + Range: range_, Color: parseColorConstraint(sourceCtx.ColorConstraint()), ValueExpr: parseValueExpr(sourceCtx.ValueExpr()), } @@ -569,7 +570,7 @@ func parseSaveStatement(saveCtx *antlrParser.SaveStatementContext) *SaveStatemen return &SaveStatement{ Range: ctxToRange(saveCtx), SentValue: parseSentValue(saveCtx.SentValue()), - Amount: parseValueExpr(saveCtx.ValueExpr()), + Account: parseValueExpr(saveCtx.ValueExpr()), } } diff --git a/numscript.go b/numscript.go index 35bc10e0..e7fa0437 100644 --- a/numscript.go +++ b/numscript.go @@ -3,6 +3,7 @@ package numscript import ( "context" + "github.com/formancehq/numscript/accounts" "github.com/formancehq/numscript/internal/interpreter" "github.com/formancehq/numscript/internal/parser" ) @@ -99,3 +100,7 @@ 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) +}