diff --git a/examples/gno.land/r/demo/multitxtest/gno.mod b/examples/gno.land/r/demo/multitxtest/gno.mod new file mode 100644 index 00000000000..5d5b8ced4ee --- /dev/null +++ b/examples/gno.land/r/demo/multitxtest/gno.mod @@ -0,0 +1 @@ +module gno.land/r/demo/multitxtest diff --git a/examples/gno.land/r/demo/multitxtest/multitx_testing.go b/examples/gno.land/r/demo/multitxtest/multitx_testing.go new file mode 100644 index 00000000000..52215e9eb80 --- /dev/null +++ b/examples/gno.land/r/demo/multitxtest/multitx_testing.go @@ -0,0 +1,62 @@ +package multitxtest + +import ( + "os" + "path/filepath" + "testing" + + "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/gnolang/gno/gnovm/stdlibs" + "github.com/gnolang/gno/gnovm/tests" + "github.com/gnolang/gno/tm2/pkg/std" +) + +type TxDefinition struct { + Pkg *std.MemPackage + Entrypoint string + OrigSend std.Coins + ExpectPanic bool +} + +func RunTxs(t *testing.T, txs []TxDefinition) { + var ( + mode = tests.ImportModeStdlibsOnly + rootDir = filepath.Join("..", "..", "..", "..", "..") + stdin = os.Stdin + stdout = os.Stdout + stderr = os.Stderr + store = tests.TestStore(rootDir, "", stdin, stdout, stderr, mode) + ) + store.SetStrictGo2GnoMapping(true) // natives must be registered + gnolang.DisableDebug() // until main call + m := tests.TestMachine(store, stdout, "main") + for i, tx := range txs { + ctx := m.Context.(stdlibs.ExecContext) + ctx.OrigSend = tx.OrigSend + if i > 0 { + ctx.Height++ + ctx.Timestamp++ + } + m.Context = ctx + memPkg := tx.Pkg + m.RunMemPackage(memPkg, true) + store.ClearCache() + m.PreprocessAllFilesAndSaveBlockNodes() + pv2 := store.GetPackage(tx.Pkg.Path, false) + if i == 0 { + m.SetActivePackage(pv2) + } + gnolang.EnableDebug() + defer func() { + r := recover() + if tx.ExpectPanic && r == nil { + t.Fatalf("expected panic in "+tx.Entrypoint+"\n%s\n", r, m.String()) + } + if !tx.ExpectPanic && r != nil { + t.Fatalf(tx.Entrypoint+" panic: %v\n%s\n", r, m.String()) + } + }() + m.RunStatement(gnolang.S(gnolang.Call(gnolang.X(tx.Entrypoint)))) + m.CheckEmpty() + } +} diff --git a/examples/gno.land/r/demo/multitxtest/slice_test.go b/examples/gno.land/r/demo/multitxtest/slice_test.go new file mode 100644 index 00000000000..d9f7e689bf2 --- /dev/null +++ b/examples/gno.land/r/demo/multitxtest/slice_test.go @@ -0,0 +1,77 @@ +package multitxtest + +import ( + "testing" + + "github.com/gnolang/gno/tm2/pkg/std" +) + +func TestSlicePopPush(t *testing.T) { + pkgName := "main" + pkgPath := "gno.land/r/demo/slicetest" + RunTxs(t, []TxDefinition{ + { + Pkg: &std.MemPackage{ + Name: pkgName, + Path: pkgPath, + Files: []*std.MemFile{ + { + Name: "main1.gno", + Body: ` + package main + import "gno.land/r/demo/multitxtest" + import "std" + func main1() { + multitxtest.Pop() + } + `, + }, + }, + }, + Entrypoint: "main1", + }, + { + Pkg: &std.MemPackage{ + Name: pkgName, + Path: pkgPath, + Files: []*std.MemFile{ + { + Name: "main2.gno", + Body: ` + package main + import "gno.land/r/demo/multitxtest" + import "std" + func main2() { + multitxtest.Push() + } + `, + }, + }, + }, + Entrypoint: "main2", + }, + { + Pkg: &std.MemPackage{ + Name: pkgName, + Path: pkgPath, + Files: []*std.MemFile{ + { + Name: "main3.gno", + Body: ` + package main + import "gno.land/r/demo/multitxtest" + import "std" + func main3() { + slice := multitxtest.GetSlice() + if len(slice) != 1 || slice[0] != "new-element" { + panic("pop/push is borked") + } + } + `, + }, + }, + }, + Entrypoint: "main3", + }, + }) +} diff --git a/examples/gno.land/r/demo/multitxtest/tests_multi.gno b/examples/gno.land/r/demo/multitxtest/tests_multi.gno new file mode 100644 index 00000000000..00c51ae24db --- /dev/null +++ b/examples/gno.land/r/demo/multitxtest/tests_multi.gno @@ -0,0 +1,15 @@ +package multitxtest + +var slice = []string{"undead-element"} + +func Pop() { + slice = slice[:len(slice)-1] +} + +func Push() { + slice = append(slice, "new-element") +} + +func GetSlice() []string { + return slice +} diff --git a/gnovm/pkg/gnolang/realm.go b/gnovm/pkg/gnolang/realm.go index 40f770c7720..fab5172ca17 100644 --- a/gnovm/pkg/gnolang/realm.go +++ b/gnovm/pkg/gnolang/realm.go @@ -121,57 +121,58 @@ func (rlm *Realm) String() string { //---------------------------------------- // ownership hooks -// po's old elem value is xo, will become co. -// po, xo, and co may each be nil. -// if rlm or po is nil, do nothing. -// xo or co is nil if the element value is undefined or has no +// base's old elem value is oldValue, will become newValue. +// base, oldValue, and newValue may each be nil. +// if rlm or base is nil, do nothing. +// oldValue or newValue is nil if the element value is undefined or has no // associated object. -func (rlm *Realm) DidUpdate(po, xo, co Object) { +func (rlm *Realm) DidUpdate(base, oldValue, newValue Object) { if rlm == nil { return } if debug { - if co != nil && co.GetIsDeleted() { + if newValue != nil && newValue.GetIsDeleted() { panic("cannot attach a deleted object") } - if po != nil && po.GetIsTransient() { + if base != nil && base.GetIsTransient() { panic("should not happen") } - if po != nil && po.GetIsDeleted() { + if base != nil && base.GetIsDeleted() { panic("cannot attach to a deleted object") } } - if po == nil || !po.GetIsReal() { + if base == nil || !base.GetIsReal() { return // do nothing. } - if po.GetObjectID().PkgID != rlm.ID { + if base.GetObjectID().PkgID != rlm.ID { panic("cannot modify external-realm or non-realm object") } - // From here on, po is real (not new-real). + // From here on, base is real (not new-real). // Updates to .newCreated/.newEscaped /.newDeleted made here. (first gen) // More appends happen during FinalizeRealmTransactions(). (second+ gen) - rlm.MarkDirty(po) - if co != nil { - co.IncRefCount() - if co.GetRefCount() > 1 { - if co.GetIsEscaped() { - // already escaped - } else { - rlm.MarkNewEscaped(co) - } - } else if co.GetIsReal() { - rlm.MarkDirty(co) + rlm.MarkDirty(base) + + updateRefs := oldValue != newValue + + if newValue != nil { + if updateRefs { + newValue.IncRefCount() + } + if newValue.GetRefCount() > 1 && !newValue.GetIsEscaped() { + rlm.MarkNewEscaped(newValue) + } else if newValue.GetIsReal() { + rlm.MarkDirty(newValue) } else { - co.SetOwner(po) - rlm.MarkNewReal(co) + newValue.SetOwner(base) + rlm.MarkNewReal(newValue) } } - if xo != nil { - xo.DecRefCount() - if xo.GetRefCount() == 0 { - if xo.GetIsReal() { - rlm.MarkNewDeleted(xo) - } + if oldValue != nil { + if updateRefs { + oldValue.DecRefCount() + } + if oldValue.GetRefCount() == 0 && oldValue.GetIsReal() { + rlm.MarkNewDeleted(oldValue) } } }