Skip to content

Commit

Permalink
feat: Bound Method Realm support (#1257)
Browse files Browse the repository at this point in the history
When a function is called, it's usually enough to get the pkgpath from
where the function is declared, to find out whether we are switching to
a new realm or not.

But when the function is a bound method, and the method and declared
type are defined in a /p/ package, we need to look at the owning
(persisting) pkgpath of the receiver, if any. When one is found, it uses
that.

There are some edge cases to consider, I'm not sure if they are all
fixed, but we will find out.
Here are some that are considered;
1. if the receiver is an unpersisted new object with method, declared in
a p module, it has no realm until it becomes persisted.
2. nil receivers aren't objects, so even if it were stored in a realm,
it would be stored by "reference" from a pointervalue which also is not
an object, so there's no realm to consider even after persistence.
3. if the method from 1 makes any modifications to the /p/ package's
block state, it should result in an error and the tx should fail.
4. if a /p/ declared thing with method gets stored in realm X, it should
be possible for realm Y to call it, and have it modify realm X (e.g. say
realm X attached a closure to the thing).

---------

Signed-off-by: moul <[email protected]>
Co-authored-by: moul <[email protected]>
  • Loading branch information
jaekwon and moul authored Jul 6, 2024
1 parent 1169658 commit 9ced20b
Show file tree
Hide file tree
Showing 12 changed files with 252 additions and 23 deletions.
1 change: 1 addition & 0 deletions examples/gno.land/p/demo/tests/p_crossrealm/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module gno.land/p/demo/tests/p_crossrealm
24 changes: 24 additions & 0 deletions examples/gno.land/p/demo/tests/p_crossrealm/p_crossrealm.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package p_crossrealm

type Stringer interface {
String() string
}

type Container struct {
A int
B Stringer
}

func (c *Container) Touch() *Container {
c.A += 1
return c
}

func (c *Container) Print() {
println("A:", c.A)
if c.B == nil {
println("B: undefined")
} else {
println("B:", c.B.String())
}
}
29 changes: 29 additions & 0 deletions examples/gno.land/r/demo/tests/crossrealm/crossrealm.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package crossrealm

import (
"gno.land/p/demo/tests/p_crossrealm"
"gno.land/p/demo/ufmt"
)

type LocalStruct struct {
A int
}

func (ls *LocalStruct) String() string {
return ufmt.Sprintf("LocalStruct{%d}", ls.A)
}

// local is saved locally in this realm
var local *LocalStruct

func init() {
local = &LocalStruct{A: 123}
}

// Make1 returns a local object wrapped by a p struct
func Make1() *p_crossrealm.Container {
return &p_crossrealm.Container{
A: 1,
B: local,
}
}
6 changes: 6 additions & 0 deletions examples/gno.land/r/demo/tests/crossrealm/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module gno.land/r/demo/tests/crossrealm

require (
gno.land/p/demo/tests/p_crossrealm v0.0.0-latest
gno.land/p/demo/ufmt v0.0.0-latest
)
2 changes: 1 addition & 1 deletion gno.land/pkg/sdk/vm/gas_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func TestAddPkgDeliverTx(t *testing.T) {
gasDeliver := gctx.GasMeter().GasConsumed()

assert.True(t, res.IsOK())
assert.Equal(t, int64(87965), gasDeliver)
assert.Equal(t, int64(91825), gasDeliver)
}

// Enough gas for a failed transaction.
Expand Down
2 changes: 1 addition & 1 deletion gnovm/cmd/gno/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func TestRunApp(t *testing.T) {
},
{
args: []string{"run", "-debug-addr", "invalidhost:17538", "../../tests/integ/debugger/sample.gno"},
errShouldContain: "listen tcp: lookup invalidhost",
errShouldContain: "listen tcp",
},
{
args: []string{"run", "../../tests/integ/invalid_assign/main.gno"},
Expand Down
71 changes: 71 additions & 0 deletions gnovm/cmd/gno/testdata/gno_test/realm_boundmethod.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Set up GNOROOT in the current directory.
mkdir $WORK/gnovm
symlink $WORK/gnovm/stdlibs -> $GNOROOT/gnovm/stdlibs
env GNOROOT=$WORK

gno test -v ./examples/gno.land/r/demo/realm2

stderr '=== RUN TestDo'
stderr '--- PASS: TestDo.*'

stderr '=== RUN file/realm2_filetest.gno'
stderr '--- PASS: file/realm2_filetest.*'

-- examples/gno.land/p/demo/counter/gno.mod --
module gno.land/p/demo/counter

-- examples/gno.land/p/demo/counter/counter.gno --
package counter

type Counter struct {
n int
}

func (c *Counter) Inc() {
c.n++
}

-- examples/gno.land/r/demo/realm1/realm1.gno --
package realm1

import "gno.land/p/demo/counter"

var c = counter.Counter{}

func GetCounter() *counter.Counter {
return &c
}

-- examples/gno.land/r/demo/realm2/realm2.gno --
package realm2

import (
"gno.land/r/demo/realm1"
)

func Do() {
realm1.GetCounter().Inc()
}

-- examples/gno.land/r/demo/realm2/realm2_filetest.gno --
// PKGPATH: gno.land/r/tests
package tests

import "gno.land/r/demo/realm2"

func main() {
realm2.Do()
println("OK")
}

// Output:
// OK

-- examples/gno.land/r/demo/realm2/realm2_test.gno --
package realm2

import "testing"

func TestDo(t *testing.T) {
Do()
}
108 changes: 93 additions & 15 deletions gnovm/pkg/gnolang/machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,14 +284,29 @@ func (m *Machine) runMemPackage(memPkg *std.MemPackage, save, overrides bool) (*
}
m.SetActivePackage(pv)
// run files.
m.RunFiles(files.Files...)
// maybe save package value and mempackage.
updates := m.RunFileDecls(files.Files...)
// save package value and mempackage.
// XXX save condition will be removed once gonative is removed.
var throwaway *Realm
if save {
// store package values and types
m.savePackageValuesAndTypes()
// store new package values and types
throwaway = m.saveNewPackageValuesAndTypes()
if throwaway != nil {
m.Realm = throwaway
}
}
// run init functions
m.runInitFromUpdates(pv, updates)
// save again after init.
if save {
m.resavePackageValues(throwaway)
// store mempackage
m.Store.AddMemPackage(memPkg)
if throwaway != nil {
m.Realm = nil
}
}

return pn, pv
}

Expand Down Expand Up @@ -494,13 +509,27 @@ func (m *Machine) injectLocOnPanic() {
}
}

// Add files to the package's *FileSet and run them.
// This will also run each init function encountered.
// Convenience for tests.
// Production must not use this, because realm package init
// must happen after persistence and realm finalization,
// then changes from init persisted again.
func (m *Machine) RunFiles(fns ...*FileNode) {
m.runFiles(fns...)
pv := m.Package
if pv == nil {
panic("RunFiles requires Machine.Package")
}
updates := m.runFileDecls(fns...)
m.runInitFromUpdates(pv, updates)
}

// Add files to the package's *FileSet and run decls in them.
// This will also run each init function encountered.
// Returns the updated typed values of package.
func (m *Machine) RunFileDecls(fns ...*FileNode) []TypedValue {
return m.runFileDecls(fns...)
}

func (m *Machine) runFiles(fns ...*FileNode) {
func (m *Machine) runFileDecls(fns ...*FileNode) []TypedValue {
// Files' package names must match the machine's active one.
// if there is one.
for _, fn := range fns {
Expand Down Expand Up @@ -628,11 +657,15 @@ func (m *Machine) runFiles(fns ...*FileNode) {
}
}

// Run new init functions.
// Go spec: "To ensure reproducible initialization
// behavior, build systems are encouraged to present
// multiple files belonging to the same package in
// lexical file name order to a compiler."
return updates
}

// Run new init functions.
// Go spec: "To ensure reproducible initialization
// behavior, build systems are encouraged to present
// multiple files belonging to the same package in
// lexical file name order to a compiler."
func (m *Machine) runInitFromUpdates(pv *PackageValue, updates []TypedValue) {
for _, tv := range updates {
if tv.IsDefined() && tv.T.Kind() == FuncKind && tv.V != nil {
fv, ok := tv.V.(*FuncValue)
Expand All @@ -651,7 +684,10 @@ func (m *Machine) runFiles(fns ...*FileNode) {

// Save the machine's package using realm finalization deep crawl.
// Also saves declared types.
func (m *Machine) savePackageValuesAndTypes() {
// This happens before any init calls.
// Returns a throwaway realm package is not a realm,
// such as stdlibs or /p/ packages.
func (m *Machine) saveNewPackageValuesAndTypes() (throwaway *Realm) {
// save package value and dependencies.
pv := m.Package
if pv.IsRealm() {
Expand All @@ -664,6 +700,7 @@ func (m *Machine) savePackageValuesAndTypes() {
rlm := NewRealm(pv.PkgPath)
rlm.MarkNewReal(pv)
rlm.FinalizeRealmTransaction(m.ReadOnly, m.Store)
throwaway = rlm
}
// save declared types.
if bv, ok := pv.Block.(*Block); ok {
Expand All @@ -675,6 +712,25 @@ func (m *Machine) savePackageValuesAndTypes() {
}
}
}
return
}

// Resave any changes to realm after init calls.
// Pass in the realm from m.saveNewPackageValuesAndTypes()
// in case a throwaway was created.
func (m *Machine) resavePackageValues(rlm *Realm) {
// save package value and dependencies.
pv := m.Package
if pv.IsRealm() {
rlm = pv.Realm
rlm.FinalizeRealmTransaction(m.ReadOnly, m.Store)
// re-save package realm info.
m.Store.SetPackageRealm(rlm)
} else { // use the throwaway realm.
rlm.FinalizeRealmTransaction(m.ReadOnly, m.Store)
}
// types were already saved, and should not change
// even after running the init function.
}

func (m *Machine) RunFunc(fn Name) {
Expand Down Expand Up @@ -815,6 +871,13 @@ func (m *Machine) RunStatement(s Stmt) {
// NOTE: to support realm persistence of types, must
// first require the validation of blocknode locations.
func (m *Machine) RunDeclaration(d Decl) {
if fd, ok := d.(*FuncDecl); ok && fd.Name == "init" {
// XXX or, consider running it, but why would this be needed?
// from a repl there is no need for init() functions.
// Also, there are complications with realms, where
// the realm must be persisted before init(), and persisted again.
panic("Machine.RunDeclaration cannot be used for init functions")
}
// Preprocess input using package block. There should only
// be one block right now, and it's a *PackageNode.
pn := m.LastBlock().GetSource(m.Store).(*PackageNode)
Expand Down Expand Up @@ -1738,8 +1801,23 @@ func (m *Machine) PushFrameCall(cx *CallExpr, fv *FuncValue, recv TypedValue) {
if pv == nil {
panic(fmt.Sprintf("package value missing in store: %s", fv.PkgPath))
}
m.Package = pv
rlm := pv.GetRealm()
if rlm == nil && recv.IsDefined() {
obj := recv.GetFirstObject(m.Store)
if obj == nil {
// could be a nil receiver.
// just ignore.
} else {
recvOID := obj.GetObjectInfo().ID
if !recvOID.IsZero() {
// override the pv and rlm with receiver's.
recvPkgOID := ObjectIDFromPkgID(recvOID.PkgID)
pv = m.Store.GetObject(recvPkgOID).(*PackageValue)
rlm = pv.GetRealm() // done
}
}
}
m.Package = pv
if rlm != nil && m.Realm != rlm {
m.Realm = rlm // enter new realm
}
Expand Down
5 changes: 0 additions & 5 deletions gnovm/pkg/gnolang/ownership.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,11 +328,6 @@ func (oi *ObjectInfo) GetIsTransient() bool {
func (tv *TypedValue) GetFirstObject(store Store) Object {
switch cv := tv.V.(type) {
case PointerValue:
// TODO: in the future, consider skipping the base if persisted
// ref-count would be 1, e.g. only this pointer refers to
// something in it; in that case, ignore the base. That will
// likely require maybe a preparation step in persistence
// ( or unlikely, a second type of ref-counting).
return cv.GetBase(store)
case *ArrayValue:
return cv
Expand Down
9 changes: 8 additions & 1 deletion gnovm/pkg/gnolang/realm.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,16 @@ func PkgIDFromPkgPath(path string) PkgID {
return PkgID{HashBytes([]byte(path))}
}

// Returns the ObjectID of the PackageValue associated with path.
func ObjectIDFromPkgPath(path string) ObjectID {
pkgID := PkgIDFromPkgPath(path)
return ObjectIDFromPkgID(pkgID)
}

// Returns the ObjectID of the PackageValue associated with pkgID.
func ObjectIDFromPkgID(pkgID PkgID) ObjectID {
return ObjectID{
PkgID: PkgIDFromPkgPath(path),
PkgID: pkgID,
NewTime: 1, // by realm logic.
}
}
Expand Down
17 changes: 17 additions & 0 deletions gnovm/tests/files/zrealm_crossrealm14.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// PKGPATH: gno.land/r/crossrealm_test
package crossrealm_test

import (
crossrealm "gno.land/r/demo/tests/crossrealm"
)

func main() {
// even though we are running within a realm,
// we aren't storing the result of crossrealm.Make1(),
// so this should print fine.
crossrealm.Make1().Touch().Print()
}

// Output:
// A: 2
// B: LocalStruct{123}
1 change: 1 addition & 0 deletions gnovm/tests/imports.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ func TestStore(rootDir, filesPath string, stdin io.Reader, stdout, stderr io.Wri
// pkg := gno.NewPackageNode(gno.Name(memPkg.Name), memPkg.Path, nil)
// pv := pkg.NewPackage()
// m2.SetActivePackage(pv)
// XXX remove second arg 'false' and remove all gonative stuff.
return m2.RunMemPackage(memPkg, false)
}
}
Expand Down

0 comments on commit 9ced20b

Please sign in to comment.