mirror of
https://github.com/hanzo-fi/numscript.git
synced 2026-10-30 20:54:52 +00:00
432 lines
11 KiB
Go
432 lines
11 KiB
Go
package interpreter
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"math/big"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/hanzo-fi/numscript/internal/analysis"
|
|
"github.com/hanzo-fi/numscript/internal/parser"
|
|
)
|
|
|
|
type Value interface {
|
|
value()
|
|
String() string
|
|
}
|
|
|
|
type String string
|
|
type Asset string
|
|
type Portion big.Rat
|
|
|
|
// AccountAddress is an account, optionally partitioned by a scope. The scope is
|
|
// a separate dimension of the account rather than part of its name, so it is
|
|
// modeled as its own field instead of being encoded into the name string.
|
|
type AccountAddress struct {
|
|
Name string
|
|
Scope string
|
|
}
|
|
|
|
type MonetaryInt big.Int
|
|
type Monetary struct {
|
|
Amount MonetaryInt
|
|
Asset Asset
|
|
}
|
|
|
|
func (String) value() {}
|
|
func (AccountAddress) value() {}
|
|
func (MonetaryInt) value() {}
|
|
func (Monetary) value() {}
|
|
func (Portion) value() {}
|
|
func (Asset) value() {}
|
|
|
|
func NewAccountAddress(src string) (AccountAddress, InterpreterError) {
|
|
if !checkAccountName(src) {
|
|
return AccountAddress{}, InvalidAccountName{Name: src}
|
|
}
|
|
return AccountAddress{Name: src}, nil
|
|
}
|
|
|
|
func NewAsset(src string) (Asset, InterpreterError) {
|
|
if !checkAssetName(src) {
|
|
return Asset(""), InvalidAsset{Name: src}
|
|
}
|
|
return Asset(src), nil
|
|
}
|
|
|
|
// A Value is (de)serialized as a tagged-JSON discriminated union, keyed by
|
|
// "type", so the on-wire form is type-explicit and unambiguous (e.g. the string
|
|
// "42" and the number 42 are distinguishable), rather than stringly-typed:
|
|
//
|
|
// string -> { "type": "string", "value": "abc" }
|
|
// number -> { "type": "number", "value": "42" }
|
|
// asset -> { "type": "asset", "name": "COIN" }
|
|
// account -> { "type": "account", "name": "x", "scope": "s" } // scope optional
|
|
// monetary -> { "type": "monetary", "asset": "COIN", "amount": "100" }
|
|
// portion -> { "type": "portion", "numerator": "1", "denominator": "2" }
|
|
const (
|
|
valueTypeString = "string"
|
|
valueTypeNumber = "number"
|
|
valueTypeAsset = "asset"
|
|
valueTypeAccount = "account"
|
|
valueTypeMonetary = "monetary"
|
|
valueTypePortion = "portion"
|
|
)
|
|
|
|
// The per-shape tagged-JSON structs below are each shared by their type's
|
|
// MarshalJSON and by ParseTaggedValue, so the layout is defined once.
|
|
//
|
|
// scalar (string/number) -> { "type": ..., "value": "..." }
|
|
// asset -> { "type": "asset", "name": "COIN" }
|
|
// account -> { "type": "account", "name": "x", "scope": "s" }
|
|
// monetary -> { "type": "monetary", "asset": "COIN", "amount": "100" }
|
|
// portion -> { "type": "portion", "numerator": "1", "denominator": "2" }
|
|
type (
|
|
taggedScalar struct {
|
|
Type string `json:"type"`
|
|
Value string `json:"value"`
|
|
}
|
|
taggedAsset struct {
|
|
Type string `json:"type"`
|
|
Name string `json:"name"`
|
|
}
|
|
taggedAccount struct {
|
|
Type string `json:"type"`
|
|
Name string `json:"name"`
|
|
Scope string `json:"scope,omitempty"`
|
|
}
|
|
taggedMonetary struct {
|
|
Type string `json:"type"`
|
|
Asset string `json:"asset"`
|
|
Amount string `json:"amount"`
|
|
}
|
|
taggedPortion struct {
|
|
Type string `json:"type"`
|
|
Numerator string `json:"numerator"`
|
|
Denominator string `json:"denominator"`
|
|
}
|
|
)
|
|
|
|
// ParseTaggedValue decodes the tagged-JSON representation of a Value. It reads
|
|
// the "type" discriminator (json can't unmarshal directly into the Value
|
|
// interface), then decodes into the struct shared with that type's MarshalJSON.
|
|
func ParseTaggedValue(data []byte) (Value, error) {
|
|
var tag struct {
|
|
Type string `json:"type"`
|
|
}
|
|
if err := json.Unmarshal(data, &tag); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch tag.Type {
|
|
case valueTypeString:
|
|
var v taggedScalar
|
|
if err := json.Unmarshal(data, &v); err != nil {
|
|
return nil, err
|
|
}
|
|
return String(v.Value), nil
|
|
|
|
case valueTypeAccount:
|
|
var v taggedAccount
|
|
if err := json.Unmarshal(data, &v); err != nil {
|
|
return nil, err
|
|
}
|
|
if !checkAccountName(v.Name) {
|
|
return nil, fmt.Errorf("invalid account name: %q", v.Name)
|
|
}
|
|
if !checkScopeName(v.Scope) {
|
|
return nil, fmt.Errorf("invalid account scope: %q", v.Scope)
|
|
}
|
|
return AccountAddress{Name: v.Name, Scope: v.Scope}, nil
|
|
|
|
case valueTypeAsset:
|
|
var v taggedAsset
|
|
if err := json.Unmarshal(data, &v); err != nil {
|
|
return nil, err
|
|
}
|
|
return Asset(v.Name), nil
|
|
|
|
case valueTypeNumber:
|
|
var v taggedScalar
|
|
if err := json.Unmarshal(data, &v); err != nil {
|
|
return nil, err
|
|
}
|
|
n, ok := new(big.Int).SetString(v.Value, 10)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid number value: %q", v.Value)
|
|
}
|
|
return MonetaryInt(*n), nil
|
|
|
|
case valueTypeMonetary:
|
|
var v taggedMonetary
|
|
if err := json.Unmarshal(data, &v); err != nil {
|
|
return nil, err
|
|
}
|
|
n, ok := new(big.Int).SetString(v.Amount, 10)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid monetary amount: %q", v.Amount)
|
|
}
|
|
return Monetary{Asset: Asset(v.Asset), Amount: MonetaryInt(*n)}, nil
|
|
|
|
case valueTypePortion:
|
|
var v taggedPortion
|
|
if err := json.Unmarshal(data, &v); err != nil {
|
|
return nil, err
|
|
}
|
|
num, ok := new(big.Int).SetString(v.Numerator, 10)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid portion numerator: %q", v.Numerator)
|
|
}
|
|
denom, ok := new(big.Int).SetString(v.Denominator, 10)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid portion denominator: %q", v.Denominator)
|
|
}
|
|
if denom.Sign() == 0 {
|
|
return nil, fmt.Errorf("invalid portion: zero denominator")
|
|
}
|
|
return Portion(*new(big.Rat).SetFrac(num, denom)), nil
|
|
|
|
case "":
|
|
return nil, fmt.Errorf("missing value type")
|
|
default:
|
|
return nil, fmt.Errorf("unknown value type: %q", tag.Type)
|
|
}
|
|
}
|
|
|
|
func (v String) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(taggedScalar{valueTypeString, string(v)})
|
|
}
|
|
|
|
func (v Asset) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(taggedAsset{valueTypeAsset, string(v)})
|
|
}
|
|
|
|
func (v AccountAddress) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(taggedAccount{valueTypeAccount, v.Name, v.Scope})
|
|
}
|
|
|
|
func (v MonetaryInt) MarshalJSON() ([]byte, error) {
|
|
bi := big.Int(v)
|
|
return json.Marshal(taggedScalar{valueTypeNumber, bi.String()})
|
|
}
|
|
|
|
func (v Portion) MarshalJSON() ([]byte, error) {
|
|
r := big.Rat(v)
|
|
return json.Marshal(taggedPortion{valueTypePortion, r.Num().String(), r.Denom().String()})
|
|
}
|
|
|
|
func (v Monetary) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(taggedMonetary{valueTypeMonetary, string(v.Asset), v.Amount.String()})
|
|
}
|
|
|
|
func (v String) String() string {
|
|
return fmt.Sprintf(`"%s"`, string(v))
|
|
}
|
|
|
|
func (v AccountAddress) String() string {
|
|
if v.Scope == "" {
|
|
return fmt.Sprintf(`@%s`, v.Name)
|
|
}
|
|
return fmt.Sprintf(`scoped(%s, "%s")`, v.Name, v.Scope)
|
|
}
|
|
|
|
func (v MonetaryInt) String() string {
|
|
i := big.Int(v)
|
|
return i.String()
|
|
}
|
|
|
|
func (v Monetary) String() string {
|
|
return fmt.Sprintf("%s %s", v.Asset, v.Amount)
|
|
}
|
|
|
|
func (p Portion) String() string {
|
|
r := big.Rat(p)
|
|
return r.String()
|
|
}
|
|
|
|
func (v Asset) String() string {
|
|
return string(v)
|
|
}
|
|
|
|
func expectMonetary(v Value, r parser.Range) (Monetary, InterpreterError) {
|
|
switch v := v.(type) {
|
|
case Monetary:
|
|
return v, nil
|
|
|
|
default:
|
|
return Monetary{}, TypeError{Expected: analysis.TypeMonetary, Value: v, Range: r}
|
|
}
|
|
}
|
|
|
|
func expectMonetaryOfAsset(expectedAsset Asset) func(Value, parser.Range) (MonetaryInt, InterpreterError) {
|
|
return func(v Value, r parser.Range) (MonetaryInt, InterpreterError) {
|
|
m, err := expectMonetary(v, r)
|
|
if err != nil {
|
|
return MonetaryInt{}, err
|
|
}
|
|
|
|
if m.Asset != expectedAsset {
|
|
return MonetaryInt{}, MismatchedCurrencyError{Expected: string(expectedAsset), Got: string(m.Asset)}
|
|
}
|
|
|
|
return m.Amount, nil
|
|
}
|
|
}
|
|
|
|
func expectNumber(v Value, r parser.Range) (MonetaryInt, InterpreterError) {
|
|
switch v := v.(type) {
|
|
case MonetaryInt:
|
|
return v, nil
|
|
|
|
default:
|
|
return MonetaryInt{}, TypeError{Expected: analysis.TypeNumber, Value: v, Range: r}
|
|
}
|
|
}
|
|
|
|
func expectString(v Value, r parser.Range) (String, InterpreterError) {
|
|
switch v := v.(type) {
|
|
case String:
|
|
return v, nil
|
|
|
|
default:
|
|
return "", TypeError{Expected: analysis.TypeString, Value: v, Range: r}
|
|
}
|
|
}
|
|
|
|
func expectAsset(v Value, r parser.Range) (Asset, InterpreterError) {
|
|
switch v := v.(type) {
|
|
case Asset:
|
|
return v, nil
|
|
|
|
default:
|
|
return "", TypeError{Expected: analysis.TypeAsset, Value: v, Range: r}
|
|
}
|
|
}
|
|
|
|
func expectAccount(v Value, r parser.Range) (AccountAddress, InterpreterError) {
|
|
switch v := v.(type) {
|
|
case AccountAddress:
|
|
return v, nil
|
|
|
|
default:
|
|
return AccountAddress{}, TypeError{Expected: analysis.TypeAccount, Value: v, Range: r}
|
|
}
|
|
}
|
|
|
|
func expectPortion(v Value, r parser.Range) (*big.Rat, InterpreterError) {
|
|
switch v := v.(type) {
|
|
case Portion:
|
|
return (*big.Rat)(&v), nil
|
|
|
|
default:
|
|
return nil, TypeError{Expected: analysis.TypePortion, Value: v, Range: r}
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
var errs []TypeError
|
|
for _, combinator := range combinators {
|
|
out, err := combinator(v, r)
|
|
if err == nil {
|
|
return out, nil
|
|
}
|
|
|
|
typeErr, ok := err.(TypeError)
|
|
if !ok {
|
|
var default_ T
|
|
return default_, err
|
|
}
|
|
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
|
|
}
|
|
|
|
var default_ T
|
|
return default_, 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 {
|
|
var default_ U
|
|
return default_, err
|
|
}
|
|
mapped := mapper(out)
|
|
return mapped, nil
|
|
}
|
|
}
|
|
|
|
func NewMonetary(asset string, n int64) Monetary {
|
|
return Monetary{
|
|
Asset: Asset(asset),
|
|
Amount: NewMonetaryInt(n),
|
|
}
|
|
}
|
|
|
|
func NewMonetaryIntBig(n *big.Int) MonetaryInt {
|
|
bi := new(big.Int).Set(n)
|
|
return MonetaryInt(*bi)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
func (asset Asset) GetBaseAndScale() (string, int64) {
|
|
parts := strings.Split(string(asset), "/")
|
|
if len(parts) == 2 {
|
|
scale, err := strconv.ParseInt(parts[1], 10, 64)
|
|
if err == nil {
|
|
return parts[0], scale
|
|
}
|
|
// fallback if parsing fails
|
|
return parts[0], 0
|
|
}
|
|
return string(asset), 0
|
|
|
|
}
|