From dda0606a08a1f2e545cefc6811abb3284659f7e8 Mon Sep 17 00:00:00 2001 From: mconcat Date: Fri, 7 Apr 2023 23:48:24 +0900 Subject: [PATCH 1/2] CreatePackage WIP --- gnovm/stdlibs/stdlibs.go | 45 ++++++++++++++++++++++++++++++++++++++++ tm2/pkg/std/memfile.go | 4 ++++ 2 files changed, 49 insertions(+) diff --git a/gnovm/stdlibs/stdlibs.go b/gnovm/stdlibs/stdlibs.go index 495482cfad2..7bc626d44fb 100644 --- a/gnovm/stdlibs/stdlibs.go +++ b/gnovm/stdlibs/stdlibs.go @@ -6,6 +6,7 @@ import ( "reflect" "strconv" "time" + "strings" gno "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/gnolang/gno/tm2/pkg/bech32" @@ -483,6 +484,50 @@ func InjectPackage(store gno.Store, pn *gno.PackageNode) { m.PushValue(res0) }, ) + + // CreatePackage creates a new package from an existing path. + // Given a package path and the new path name, with optional trailing arguments for init, + // it creates a new package under the prefix of the current package. + // + // Example: a "template" GRC20 package is created under the std package("std/grc20"). + // The init() of grc20 templates takes two arguments: the name of the token and the initial supply. + // To create a new GRC20 package from package "tokenfactory", we can use the following code: + // ```gno + // mytokenPackage := std.CreatePackage("std/grc20", "tokens/mytoken", 10000000000000).(IGRC20); + // ``` + // This will create a new package under the current package, with the path "gno.land/r/tokenfactory/tokens/mytoken/grc20". + // The result is an interface{}, exposing all public functions of the package. + // Internally, the interface maps function names to internal native call, `CallPackageFunction`. + pn.DefineNative("CreatePackage", + gno.Flds( + "pkgTemplatePath", "string", + "pkgPath", "string", + "arguments", "...interface{}", + ), + gno.Flds( + "pkg", "interface{}", + ), + func(m *gno.Machine) { + arg0, arg1, arg2 := m.LastBlock().GetParams3() + pkgTemplatePath := arg0.TV.GetString() + pkgPath := strings.Join([]string{m.Package.PkgPath, arg1.TV.GetString()}, "/") + initArgs := arg2.TV.V.(*gno.SliceValue) + + // TODO: validate pkgPath + tpkg := m.Store.Fork().GetMemPackage(pkgTemplatePath) + tpkg.RenamePath(pkgPath) + + // TODO: this might be insecure due to shared execution environment. + _, pkg := m.RunMemPackage(tpkg, true) + + res0 := gno.Go2GnoValue( + m.Alloc, + m.Store, + reflect.ValueOf(pkg), + ) + m.PushValue(res0) + }, + ) } } diff --git a/tm2/pkg/std/memfile.go b/tm2/pkg/std/memfile.go index 55c384485e7..aefd996e44e 100644 --- a/tm2/pkg/std/memfile.go +++ b/tm2/pkg/std/memfile.go @@ -21,6 +21,10 @@ type MemPackage struct { Files []*MemFile } +func (mempkg *MemPackage) RenamePath(newPath string) { + mempkg.Path = newPath +} + func (mempkg *MemPackage) GetFile(name string) *MemFile { for _, memFile := range mempkg.Files { if memFile.Name == name { From ccf415462f89c28945e2bac340880172d85ce539 Mon Sep 17 00:00:00 2001 From: mconcat Date: Thu, 13 Apr 2023 14:26:43 +0900 Subject: [PATCH 2/2] in progress --- gnovm/pkg/gnolang/nodes.go | 8 ++ gnovm/pkg/gnolang/values.go | 46 +++++++++++ gnovm/stdlibs/stdlibs.go | 148 ++++++++++++++++++++++++++++++++---- gnovm/tests/call_test.go | 15 ++++ 4 files changed, 203 insertions(+), 14 deletions(-) create mode 100644 gnovm/tests/call_test.go diff --git a/gnovm/pkg/gnolang/nodes.go b/gnovm/pkg/gnolang/nodes.go index d8aa1b66477..31fbab1f1a5 100644 --- a/gnovm/pkg/gnolang/nodes.go +++ b/gnovm/pkg/gnolang/nodes.go @@ -9,6 +9,7 @@ import ( "regexp" "strconv" "strings" + "unicode" "github.com/gnolang/gno/tm2/pkg/errors" "github.com/gnolang/gno/tm2/pkg/std" @@ -109,6 +110,13 @@ const ( type Name string +// Returns if the name is an exported identifier, +// according to the golang's canonical exported name rule, +// namely, starting with a capitalized alphabet. +func (name Name) IsExportedName() bool { + return unicode.IsUpper([]rune(name)[0]) +} + // ---------------------------------------- // Location // Acts as an identifier for nodes. diff --git a/gnovm/pkg/gnolang/values.go b/gnovm/pkg/gnolang/values.go index 495fcf74cb7..2b82894f9c8 100644 --- a/gnovm/pkg/gnolang/values.go +++ b/gnovm/pkg/gnolang/values.go @@ -891,6 +891,21 @@ func (pv *PackageValue) GetPkgAddr() crypto.Address { return DerivePkgAddr(pv.PkgPath) } +func (pv *PackageValue) GetExportedFunctions(store Store) []*FuncDecl { + res := []*FuncDecl{} + for _, file := range pv.GetPackageNode(store).FileSet.Files { + for _, decl := range file.Decls { + if fdecl, ok := decl.(*FuncDecl); ok { + if !fdecl.NameExpr.Name.IsExportedName() { + continue + } + res = append(res, fdecl) + } + } + } + return res +} + // ---------------------------------------- // NativeValue @@ -1521,6 +1536,37 @@ func (tv *TypedValue) ComputeMapKey(store Store, omitType bool) MapKey { return MapKey(bz) } +// - PrimitiveValue is moveable across realm boundary. +// - Array and Struct are moveable across realm only if they are composed of moveable types(recursively applied). The value will be copied to the callee realm. +// - Slice are moveable across realm only if they are referring to moveable types(recursively applied)(not implemented yet). +// - Any other cases require some complex inter-realm object management scheme, low priority. +func (tv *TypedValue) IsMoveableAcrossRealm() bool { + switch tv.T.Kind() { + case + BoolKind, StringKind, + IntKind, Int8Kind, Int16Kind, Int32Kind, Int64Kind, + UintKind, Uint8Kind, Uint16Kind, Uint32Kind, Uint64Kind, + BigintKind, BigdecKind: + return true + case StructKind: + for _, field := range tv.V.(*StructValue).Fields { + if !field.IsMoveableAcrossRealm() { + return false + } + } + return true + case ArrayKind: + for _, field := range tv.V.(*ArrayValue).List { + if !field.IsMoveableAcrossRealm() { + return false + } + } + return true + default: + return false + } +} + // ---------------------------------------- // Value utility/manipulation functions. diff --git a/gnovm/stdlibs/stdlibs.go b/gnovm/stdlibs/stdlibs.go index 7bc626d44fb..873fb479262 100644 --- a/gnovm/stdlibs/stdlibs.go +++ b/gnovm/stdlibs/stdlibs.go @@ -2,11 +2,14 @@ package stdlibs import ( "crypto/sha256" + "fmt" "math" + "os" "reflect" "strconv" - "time" "strings" + "time" + "unicode" gno "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/gnolang/gno/tm2/pkg/bech32" @@ -483,8 +486,108 @@ func InjectPackage(store gno.Store, pn *gno.PackageNode) { res0.T = addrT m.PushValue(res0) }, - ) + ) /* + pn.DefineNative("LoadPackage", + gno.Flds( + "pkgPath", "string", + ), + gno.Flds( + "pkg", gno.AnyT(), + ), + func (m *gno.Machine) { + arg0 := m.LastBlock().GetParam1() + pkgPath := arg0.TV.GetString() + fdecls := m.Store.GetPackage(pkgPath).GetExportedFunctions(m.Store) + + fields := []*gno.FieldType{} + + for _, fdecl := range fdecls { + field := gno.FieldType{ + Name: fdecl.Name, + + } + } + + res0 := gno.Go2GnoValue( + m.Alloc, + m.Store, + reflect.ValueOf(pkg), + ) + } + )*/ + pn.DefineNative("CallPackage1", + gno.Flds( + "pkgPath", "string", + "functionName", "string", + "args", &gno.SliceTypeExpr{Elt: gno.AnyT(), Vrd: true}, + ), + gno.Flds( + "returnValue1", gno.AnyT(), + ), + func(m *gno.Machine) { + arg0, arg1, arg2 := m.LastBlock().GetParams3() + pkgPath := arg0.TV.GetString() + functionName := arg1.TV.GetString() + if !unicode.IsUpper([]rune(functionName)[0]) { + panic("functionName is not exported") + } + args := arg2.TV.V.(*gno.SliceValue).GetBase(m.Store).List + + pv := m.Store.GetPackage(pkgPath, false) + pl := gno.PackageNodeLocation(pkgPath) + pn := store.GetBlockNode(pl).(*gno.PackageNode) + ft := pn.GetStaticTypeOf(store, gno.Name(functionName)).(*gno.FuncType) + + mpn := gno.NewPackageNode("main", "main", nil) + mpn.Define("pkg", gno.TypedValue{T: &gno.PackageType{}, V: pv}) + mpv := mpn.NewPackage() + + // The new environment should be able to access the provided arguments, but not any other caller realm components(variables etc.). + // We achieve this by: + // - Use a fresh gno.Machine for executing a function from dynamically imported package + // - Limiting the set of moveable arguments by checking arg.IsMoveableAcrossRealm(), making sure there is no inter-realm reference. + // - Eval() the CallExpr using the arguments above. + + if len(ft.Params) != len(args) { + panic("Argument length does not match") + } + argexprs := make([]interface{}, len(args)) + for i, arg := range args { + if !arg.IsMoveableAcrossRealm() { + panic("Argument cannot pass realm boundary") + } + argexprs[i] = &gno.ConstExpr{ + TypedValue: arg, + } + } + + mm := gno.NewMachineWithOptions( + gno.MachineOptions{ + PkgPath: pkgPath, + Output: os.Stdout, + Store: m.Store, + Context: m.Context, + Alloc: m.Alloc, + MaxCycles: m.MaxCycles - m.Cycles, + }, + ) + mm.SetActivePackage(mpv) + + // Call the function. + call := gno.Call(fmt.Sprintf("pkg.%s", functionName), argexprs...) + tvs := mm.Eval(call) + if len(tvs) != 1 { + panic("Return argument is not length 1") + } + + m.Cycles -= mm.Cycles + + mm.Release() + + m.PushValue(tvs[0]) + }, + ) // CreatePackage creates a new package from an existing path. // Given a package path and the new path name, with optional trailing arguments for init, // it creates a new package under the prefix of the current package. @@ -497,7 +600,7 @@ func InjectPackage(store gno.Store, pn *gno.PackageNode) { // ``` // This will create a new package under the current package, with the path "gno.land/r/tokenfactory/tokens/mytoken/grc20". // The result is an interface{}, exposing all public functions of the package. - // Internally, the interface maps function names to internal native call, `CallPackageFunction`. + // Internally, the interface maps function names to internal native call, `CallPackage`. pn.DefineNative("CreatePackage", gno.Flds( "pkgTemplatePath", "string", @@ -505,27 +608,44 @@ func InjectPackage(store gno.Store, pn *gno.PackageNode) { "arguments", "...interface{}", ), gno.Flds( - "pkg", "interface{}", + "pkg", "string", ), func(m *gno.Machine) { arg0, arg1, arg2 := m.LastBlock().GetParams3() pkgTemplatePath := arg0.TV.GetString() pkgPath := strings.Join([]string{m.Package.PkgPath, arg1.TV.GetString()}, "/") - initArgs := arg2.TV.V.(*gno.SliceValue) + initArgs := arg2.TV.V.(*gno.SliceValue).GetBase(m.Store).List + if len(initArgs) != 0 { + panic("not implemented: initArgs") + } + if pv := m.Store.GetPackage(pkgPath, false); pv != nil { + panic("package already exists: " + pkgPath) + } // TODO: validate pkgPath - tpkg := m.Store.Fork().GetMemPackage(pkgTemplatePath) + tpkg := m.Store.GetMemPackage(pkgTemplatePath) tpkg.RenamePath(pkgPath) - // TODO: this might be insecure due to shared execution environment. - _, pkg := m.RunMemPackage(tpkg, true) - - res0 := gno.Go2GnoValue( - m.Alloc, - m.Store, - reflect.ValueOf(pkg), + mm := gno.NewMachineWithOptions( + gno.MachineOptions{ + PkgPath: "", + Output: os.Stdout, + Store: m.Store, + Alloc: m.Alloc, + Context: m.Context, + MaxCycles: m.MaxCycles - m.Cycles, + }, ) - m.PushValue(res0) + + mm.RunMemPackage(tpkg, true) + + m.Cycles -= mm.Cycles + + mm.Release() + + var ret gno.TypedValue + ret.SetString(gno.StringValue(pkgPath)) + m.PushValue(ret) }, ) } diff --git a/gnovm/tests/call_test.go b/gnovm/tests/call_test.go new file mode 100644 index 00000000000..ea6a1b0f0b8 --- /dev/null +++ b/gnovm/tests/call_test.go @@ -0,0 +1,15 @@ +package tests + +import ( + "testing" + "bytes" + + gno "github.com/gnolang/gno/gnovm/pkg/gnolang" +) + +func TestCreatePackage(t *testing.T) { + stdin := new(bytes.Buffer) + stdout := os.Stdout + stderr := new(bytes.Buffer) + store := TestStore("..", "", stdin) +} \ No newline at end of file