diff --git a/pkgs/encoding/json/encode.go b/pkgs/encoding/json/encode.go new file mode 100644 index 00000000000..b585fb2626c --- /dev/null +++ b/pkgs/encoding/json/encode.go @@ -0,0 +1,226 @@ +package json + +import ( + "bytes" + "strconv" + "sync" + + gno "github.com/gnolang/gno/pkgs/gnolang" +) + +// Marshal returns the JSON encoding of v. +func Marshal(s gno.Store, vs ...gno.TypedValue) ([]byte, error) { + e := newEncodeState() + defer encodeStatePool.Put(e) + + if len(vs) > 1 { + e.WriteByte('[') + } + + for i, v := range vs { + if i > 0 { + e.WriteByte(',') + } + err := e.marshal(v, s, encOpts{escapeHTML: true}) + if err != nil { + return nil, err + } + } + + if len(vs) > 1 { + e.WriteByte(']') + } + + buf := append([]byte(nil), e.Bytes()...) + + return buf, nil +} + +type encodeState struct { + bytes.Buffer // accumulated output +} + +var encodeStatePool sync.Pool + +func newEncodeState() *encodeState { + if v := encodeStatePool.Get(); v != nil { + e := v.(*encodeState) + e.Reset() + return e + } + + return &encodeState{} +} + +// jsonError is an error wrapper type for internal use only. +type jsonError struct{ error } + +func (e *encodeState) marshal(tv gno.TypedValue, s gno.Store, opts encOpts) (err error) { + defer func() { + if r := recover(); r != nil { + if je, ok := r.(jsonError); ok { + err = je.error + } else { + panic(r) + } + } + }() + + v := newValue(tv, s) + valueEncoder(v)(e, v, opts) + return nil +} + +type encOpts struct { + escapeHTML bool + quoted bool +} + +type encoderFunc func(e *encodeState, v Value, opts encOpts) + +func valueEncoder(v Value) encoderFunc { + switch v.Kind() { + case gno.InvalidKind: + return invalidValueEncoder + case gno.BoolKind: + return boolEncoder + case gno.IntKind, gno.Int8Kind, gno.Int16Kind, gno.Int32Kind, gno.Int64Kind: + return intEncoder + case gno.UintKind, gno.Uint8Kind, gno.Uint16Kind, gno.Uint32Kind, gno.Uint64Kind: + return uintEncoder + case gno.StringKind: + return stringEncoder + case gno.ArrayKind: + return arrayEncoder + case gno.SliceKind: + return sliceEncoder + case gno.StructKind: + return structEncoder + case gno.PointerKind: + return ptrEncoder + case gno.InterfaceKind: + return interfaceEncoder + default: + panic("unreachable") //todo: unsupportedTypeEncoder? + } +} + +func invalidValueEncoder(e *encodeState, v Value, _ encOpts) { + e.WriteString("null") +} + +func boolEncoder(e *encodeState, v Value, opts encOpts) { + if opts.quoted { + e.WriteByte('"') + } + if v.Bool() { + e.WriteString("true") + } else { + e.WriteString("false") + } + if opts.quoted { + e.WriteByte('"') + } +} + +func stringEncoder(e *encodeState, v Value, opts encOpts) { + e.WriteByte('"') + e.WriteString(v.String()) + e.WriteByte('"') +} + +func intEncoder(e *encodeState, v Value, opts encOpts) { + if opts.quoted { + e.WriteByte('"') + } + + e.WriteString(strconv.FormatInt(v.Int(), 10)) + + if opts.quoted { + e.WriteByte('"') + } +} + +func uintEncoder(e *encodeState, v Value, opts encOpts) { + if opts.quoted { + e.WriteByte('"') + } + + e.WriteString(strconv.FormatUint(v.Uint(), 10)) + + if opts.quoted { + e.WriteByte('"') + } +} + +func arrayEncoder(e *encodeState, v Value, opts encOpts) { + e.WriteByte('[') + + for i := 0; i < v.Len(); i++ { + if i > 0 { + e.WriteByte(',') + } + elem := v.Index(i) + valueEncoder(elem)(e, elem, opts) + } + + e.WriteByte(']') +} + +func sliceEncoder(e *encodeState, v Value, opts encOpts) { + e.WriteByte('[') + + for i := 0; i < v.Len(); i++ { + if i > 0 { + e.WriteByte(',') + } + elem := v.Index(i) + valueEncoder(elem)(e, elem, opts) + } + + e.WriteByte(']') +} + +func structEncoder(e *encodeState, v Value, opts encOpts) { + next := byte('{') + + fields := v.StructFields() + + for i := 0; i < len(fields); i++ { + field := fields[i] + if field.IsZero() { + continue + } + e.WriteByte(next) + next = ',' + + e.WriteByte('"') + e.WriteString(field.Name()) + e.WriteByte('"') + e.WriteByte(':') + + valueEncoder(field.Value())(e, field.Value(), opts) + } + + if next == '{' { + e.WriteString("{}") + } else { + e.WriteByte('}') + } +} + +func ptrEncoder(e *encodeState, v Value, opts encOpts) { + if v.IsNil() { + e.WriteString("null") + return + } + valueEncoder(v.Elem())(e, v.Elem(), opts) +} + +func interfaceEncoder(e *encodeState, v Value, opts encOpts) { + if v.IsNil() { + e.WriteString("null") + return + } + valueEncoder(v.Elem())(e, v.Elem(), opts) +} diff --git a/pkgs/encoding/json/encode_test.go b/pkgs/encoding/json/encode_test.go new file mode 100644 index 00000000000..7dd52d42879 --- /dev/null +++ b/pkgs/encoding/json/encode_test.go @@ -0,0 +1,91 @@ +package json + +import ( + "reflect" + "testing" + + gno "github.com/gnolang/gno/pkgs/gnolang" +) + +func TestMarshal(t *testing.T) { + var tests = []struct { + name string + in any + want string + }{ + {"nil", nil, "null"}, + {"bool", true, "true"}, + {"bool", false, "false"}, + {"int", int(0), "0"}, + {"int", int8(10), "10"}, + {"int", int16(100), "100"}, + {"int", int32(1000), "1000"}, + {"int", int64(10000), "10000"}, + {"array", [2]int{1, 2}, "[1,2]"}, + {"slice", make([]int, 0), "[]"}, + {"string", "hello", "\"hello\""}, + {"struct", struct { + A int + B string + c string + }{23, "skidoo", "aa"}, `{"A":23,"B":"skidoo"}`}, + {"struct-tag", struct { + A int `json:"a"` + B string + }{23, "skidoo"}, `{"a":23,"B":"skidoo"}`}, + {"pointer", &struct { + A int + B string + }{23, "skidoo"}, `{"A":23,"B":"skidoo"}`}, + //{"map", map[string]int{"one": 1, "two": 2}, `{"one":1,"two":2}`}, + } + for _, tt := range tests { + testMarshal := func(t *testing.T, fn func(v any) (gno.TypedValue, gno.Store)) { + v, s := fn(tt.in) + + /* + if v.T != nil { + fmt.Printf("t: %T v: %T k: %v\n", v.T, v.V, v.T.Kind()) + } + */ + + got, err := Marshal(s, v) + //fmt.Printf("got: %s\n", string(got)) + if err != nil { + t.Errorf("Marshal(%v) error: %v", tt.in, err) + } + if string(got) != tt.want { + t.Errorf("Marshal(%v) = %v, want %v", tt.in, string(got), tt.want) + } + t.Logf("Marshal(%v) = %v", tt.in, string(got)) + } + + t.Run(tt.name, func(t *testing.T) { + testMarshal(t, go2GnoTypedValue) + }) + t.Run(tt.name+"Native", func(t *testing.T) { + testMarshal(t, go2GnoTypedValueNative) + }) + } +} + +func go2GnoTypedValueNative(v any) (gno.TypedValue, gno.Store) { + //alloc := gno.NewAllocator(0) + alloc := (*gno.Allocator)(nil) + store := gno.NewStore(alloc, nil, nil) + rv := reflect.ValueOf(v) + btv := gno.Go2GnoNativeValue(alloc, rv) + return btv, store +} + +func go2GnoTypedValue(v any) (gno.TypedValue, gno.Store) { + alloc := (*gno.Allocator)(nil) + //alloc := gno.NewAllocator(0) + store := gno.NewStore(alloc, nil, nil) + if v == nil { + return gno.TypedValue{}, store + } + rv := reflect.ValueOf(v) + btv := gno.Go2GnoValue(alloc, store, rv) + return btv, store +} diff --git a/pkgs/encoding/json/struct.go b/pkgs/encoding/json/struct.go new file mode 100644 index 00000000000..09adaf82c2b --- /dev/null +++ b/pkgs/encoding/json/struct.go @@ -0,0 +1,82 @@ +package json + +import ( + "reflect" + "strings" + + gno "github.com/gnolang/gno/pkgs/gnolang" +) + +type StructField interface { + IsZero() bool + Name() string + Value() Value +} + +type gnoStructField struct { + fieldType gno.FieldType + value Value +} + +type tagOptions string + +func (sf gnoStructField) IsZero() bool { + return sf.value.IsZero() +} + +func (sf gnoStructField) Name() string { + stag := reflect.StructTag(string(sf.fieldType.Tag)) + tag := stag.Get("json") + name, _ := parseTag(tag) + // TODO: handle omitempty + /* + if !isValidTag(name) { + name = "" + } + */ + + if name == "" { + name = string(sf.fieldType.Name) + } + + return name +} + +func (sf gnoStructField) Value() Value { + return sf.value +} + +type nativeStructField struct { + field reflect.StructField + value Value +} + +func (sf nativeStructField) IsZero() bool { + return sf.value.IsZero() +} + +func (sf nativeStructField) Name() string { + tag := sf.field.Tag.Get("json") + name, _ := parseTag(tag) + //TODO: handle omitempty + /* + if !isValidTag(name) { + name = "" + } + */ + + if name == "" { + name = sf.field.Name + } + + return name +} + +func (sf nativeStructField) Value() Value { + return sf.value +} + +func parseTag(tag string) (string, tagOptions) { + tag, opt, _ := strings.Cut(tag, ",") + return tag, tagOptions(opt) +} diff --git a/pkgs/encoding/json/value.go b/pkgs/encoding/json/value.go new file mode 100644 index 00000000000..7b46a434370 --- /dev/null +++ b/pkgs/encoding/json/value.go @@ -0,0 +1,211 @@ +package json + +import ( + "reflect" + "unicode" + + gno "github.com/gnolang/gno/pkgs/gnolang" +) + +type Value interface { + Kind() gno.Kind + + // Array & Slice & Map + Len() int + Index(i int) Value + + // Struct + StructFields() []StructField + + // Elem returns the value that the interface v contains or that the pointer v points to. + Elem() Value + + String() string + Bool() bool + Int() int64 + Uint() uint64 + + IsNil() bool + IsZero() bool +} + +type gnoValue struct { + v gno.TypedValue + s gno.Store +} + +type nativeValue struct { + v reflect.Value +} + +func newValue(tv gno.TypedValue, s gno.Store) Value { + if nv, ok := tv.V.(*gno.NativeValue); ok { + return nativeValue{nv.Value} + } + return gnoValue{tv, s} +} + +func (gv gnoValue) Kind() gno.Kind { + if gv.v.T == nil { + return gno.InvalidKind + } + return gv.v.T.Kind() +} + +func (gv gnoValue) Len() int { + return gv.v.GetLength() +} + +func (gv gnoValue) Index(i int) Value { + switch v := gv.v.V.(type) { + case *gno.ArrayValue: + return gnoValue{v.List[i], gv.s} + case *gno.SliceValue: + sv := v.GetBase(gv.s) + return gnoValue{sv.List[i], gv.s} + } + // TODO panic + panic("should not happen") +} + +func (gv gnoValue) String() string { + return gv.v.GetString() +} + +func (gv gnoValue) Int() int64 { + return gv.v.GetInt64() +} + +func (gv gnoValue) Uint() uint64 { + return gv.v.GetUint64() +} + +func (gv gnoValue) Bool() bool { + return gv.v.GetBool() +} + +func (gv gnoValue) IsNil() bool { + return gv.v.IsUndefined() +} + +func (gv gnoValue) IsZero() bool { + // todo + return false +} + +func (gv gnoValue) StructFields() []StructField { + if gv.v.T.Kind() != gno.StructKind { + panic("not a struct") + } + + var stt *gno.StructType + if dt, ok := gv.v.T.(*gno.DeclaredType); ok { + stt = dt.Base.(*gno.StructType) + } else { + stt = gv.v.T.(*gno.StructType) + } + + stv := gv.v.V.(*gno.StructValue) + + var fields []StructField + + for i, ft := range stt.Fields { + fname := string(ft.Name) + if fname == "" || !isFirstCharUpper(fname) { + continue + } + fields = append(fields, gnoStructField{ + fieldType: ft, + value: newValue(stv.Fields[i], gv.s), + }) + } + return fields +} + +func (gv gnoValue) Elem() Value { + //todo: check kind is interface or pointer + if gv.v.T.Kind() == gno.PointerKind { + pv := gv.v.V.(gno.PointerValue) + return newValue(pv.Deref(), gv.s) + } + return nil +} + +func (nv nativeValue) Kind() gno.Kind { + switch nv.v.Kind() { + case reflect.Bool: + return gno.BoolKind + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return gno.IntKind + case reflect.String: + return gno.StringKind + case reflect.Array: + return gno.ArrayKind + case reflect.Slice: + return gno.SliceKind + case reflect.Struct: + return gno.StructKind + case reflect.Pointer: + return gno.PointerKind + case reflect.Interface: + return gno.InterfaceKind + } + return gno.InvalidKind +} + +func (nv nativeValue) Len() int { + return nv.v.Len() +} + +func (nv nativeValue) Index(i int) Value { + return nativeValue{nv.v.Index(i)} +} + +func (nv nativeValue) String() string { + return nv.v.String() +} + +func (nv nativeValue) Int() int64 { + return nv.v.Int() +} + +func (nv nativeValue) Uint() uint64 { + return nv.v.Uint() +} + +func (nv nativeValue) Bool() bool { + return nv.v.Bool() +} + +func (nv nativeValue) IsNil() bool { + return nv.v.IsNil() +} + +func (nv nativeValue) IsZero() bool { + return nv.v.IsZero() +} + +//TODO +func (nv nativeValue) StructFields() []StructField { + var fields []StructField + for i := 0; i < nv.v.NumField(); i++ { + fname := nv.v.Type().Field(i).Name + if fname == "" || !isFirstCharUpper(fname) { + continue + } + fields = append(fields, nativeStructField{ + field: nv.v.Type().Field(i), + value: nativeValue{nv.v.Field(i)}, + }) + } + return fields +} + +func (nv nativeValue) Elem() Value { + return nativeValue{nv.v.Elem()} +} + +func isFirstCharUpper(s string) bool { + firstChar := []rune(s)[0] + return unicode.IsUpper(firstChar) +} diff --git a/pkgs/gnolang/gonative.go b/pkgs/gnolang/gonative.go index 28cae4f7e08..7956dedc3d9 100644 --- a/pkgs/gnolang/gonative.go +++ b/pkgs/gnolang/gonative.go @@ -286,6 +286,7 @@ func (ds *defaultStore) Go2GnoType(rt reflect.Type) (t Type) { fs[i] = FieldType{ Name: Name(sf.Name), Type: go2GnoType(sf.Type), + Tag: Tag(string(sf.Tag)), } } st.PkgPath = rt.PkgPath() diff --git a/pkgs/sdk/vm/handler.go b/pkgs/sdk/vm/handler.go index 4a091e1850a..44c7471b1d6 100644 --- a/pkgs/sdk/vm/handler.go +++ b/pkgs/sdk/vm/handler.go @@ -170,7 +170,7 @@ func (vh vmHandler) queryEval(ctx sdk.Context, req abci.RequestQuery) (res abci. } pkgPath := reqParts[0] expr := reqParts[1] - result, err := vh.vm.QueryEval(ctx, pkgPath, expr) + result, err := vh.vm.QueryEvalJSON(ctx, pkgPath, expr) if err != nil { res = sdk.ABCIResponseQueryFromError(err) return diff --git a/pkgs/sdk/vm/keeper.go b/pkgs/sdk/vm/keeper.go index b9b72889504..1498952d823 100644 --- a/pkgs/sdk/vm/keeper.go +++ b/pkgs/sdk/vm/keeper.go @@ -5,6 +5,7 @@ import ( "os" "strings" + "github.com/gnolang/gno/pkgs/encoding/json" "github.com/gnolang/gno/pkgs/errors" gno "github.com/gnolang/gno/pkgs/gnolang" "github.com/gnolang/gno/pkgs/sdk" @@ -442,6 +443,56 @@ func (vm *VMKeeper) QueryEvalString(ctx sdk.Context, pkgPath string, expr string return res, nil } +func (vm *VMKeeper) QueryEvalJSON(ctx sdk.Context, pkgPath string, expr string) (res string, err error) { + alloc := gno.NewAllocator(maxAllocQuery) + store := vm.getGnoStore(ctx) + pkgAddr := gno.DerivePkgAddr(pkgPath) + // Get Package. + pv := store.GetPackage(pkgPath, false) + if pv == nil { + err = ErrInvalidPkgPath(fmt.Sprintf( + "package not found: %s", pkgPath)) + return "", err + } + // Parse expression. + xx, err := gno.ParseExpr(expr) + if err != nil { + return "", err + } + // Construct new machine. + msgCtx := stdlibs.ExecContext{ + ChainID: ctx.ChainID(), + Height: ctx.BlockHeight(), + Timestamp: ctx.BlockTime().Unix(), + // Msg: msg, + // OrigCaller: caller, + // OrigSend: jsend, + // OrigSendSpent: nil, + OrigPkgAddr: pkgAddr.Bech32(), + Banker: NewSDKBanker(vm, ctx), // safe as long as ctx is a fork to be discarded. + } + m := gno.NewMachineWithOptions( + gno.MachineOptions{ + PkgPath: pkgPath, + Output: os.Stdout, // XXX + Store: store, + Context: msgCtx, + Alloc: alloc, + MaxCycles: 10 * 1000 * 1000, // 10M cycles // XXX + }) + rtvs := m.Eval(xx) + if len(rtvs) == 0 { + return "", errors.New("expected result, got none") + } + + bs, err := json.Marshal(store, rtvs...) + if err != nil { + return "", err + } + res = string(bs) + return res, nil +} + func (vm *VMKeeper) QueryFile(ctx sdk.Context, filepath string) (res string, err error) { store := vm.getGnoStore(ctx) dirpath, filename := std.SplitFilepath(filepath) diff --git a/pkgs/sdk/vm/keeper_test.go b/pkgs/sdk/vm/keeper_test.go index 63900d66c78..c223718e0ac 100644 --- a/pkgs/sdk/vm/keeper_test.go +++ b/pkgs/sdk/vm/keeper_test.go @@ -295,3 +295,46 @@ func GetAdmin() string { assert.NoError(t, err) assert.Equal(t, res, addrString) } + +func TestVMKeeperEvalJSON(t *testing.T) { + env := setupTestEnv() + ctx := env.ctx + + // Give "addr1" some gnots. + addr := crypto.AddressFromPreimage([]byte("addr1")) + acc := env.acck.NewAccountWithAddress(ctx, addr) + env.acck.SetAccount(ctx, acc) + env.bank.SetCoins(ctx, addr, std.MustParseCoins("10000000ugnot")) + assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins("10000000ugnot"))) + + // Create test package + files := []*std.MemFile{ + {"init.gno", ` +package test + +type Result struct { + A string + B int +} + +func QueryStruct() (*Result) { + r := &Result{ + A: "hello", + B: 123, + } + return r +} +`}, + } + pkgPath := "gno.land/r/test" + msg1 := NewMsgAddPackage(addr, pkgPath, files) + err := env.vmk.AddPackage(ctx, msg1) + assert.NoError(t, err) + + // Run QueryStruct() + res, err := env.vmk.QueryEvalJSON(ctx, pkgPath, "QueryStruct()") + assert.NoError(t, err) + + // Check result. + assert.Equal(t, res, `{"A":"hello","B":123}`) +}