From 4135ffe88369ab23b01945ab73989553d043c6d9 Mon Sep 17 00:00:00 2001 From: Emmanuel T Odeke Date: Mon, 20 Jan 2025 00:42:33 -0800 Subject: [PATCH 01/12] fix(gnodev/pkg/emitter): use html/template not text/template for HTML generation (#3545) This change uses html/template instead of text/template for HTML generation and also locks in tests to detect such subtle regressions and thus help prevent future cross-side scripting (XSS) attacks if later the scripts evolve and take in user input. Fixes #3544 --- contribs/gnodev/pkg/emitter/middleware.go | 2 +- .../gnodev/pkg/emitter/middleware_test.go | 43 +++++++++++++++++++ .../gnodev/pkg/emitter/static/hotreload.js | 4 +- 3 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 contribs/gnodev/pkg/emitter/middleware_test.go diff --git a/contribs/gnodev/pkg/emitter/middleware.go b/contribs/gnodev/pkg/emitter/middleware.go index 9c53cfe158e..e4def43f919 100644 --- a/contribs/gnodev/pkg/emitter/middleware.go +++ b/contribs/gnodev/pkg/emitter/middleware.go @@ -5,10 +5,10 @@ import ( _ "embed" "encoding/json" "fmt" + "html/template" "net/http" "strings" "sync" - "text/template" "github.com/gnolang/gno/contribs/gnodev/pkg/events" ) diff --git a/contribs/gnodev/pkg/emitter/middleware_test.go b/contribs/gnodev/pkg/emitter/middleware_test.go new file mode 100644 index 00000000000..bed7ddd0e75 --- /dev/null +++ b/contribs/gnodev/pkg/emitter/middleware_test.go @@ -0,0 +1,43 @@ +package emitter + +import ( + "fmt" + "net/http" + "net/http/httptest" + "regexp" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMiddlewareUsesHTMLTemplate(t *testing.T) { + tests := []struct { + name string + remote string + want string + }{ + {"normal remote", "localhost:9999", "const ws = new WebSocket('ws://localhost:9999');"}, + {"xss'd remote", `localhost:9999');alert('pwned`, "const ws = new WebSocket('ws://localhost:9999');alert('pwned');"}, + } + + // As the code revolves, add more search patterns here. + reWebsocket := regexp.MustCompile("const ws = new WebSocket[^\n]+") + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rec := httptest.NewRecorder() + mdw := NewMiddleware(tt.remote, http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.Header().Set("Content-Type", "text/html") + fmt.Fprintf(rw, "") + })) + rec.Header().Set("Content-Type", "text/html") + req := httptest.NewRequest("GET", "https://gno.land/example", nil) + mdw.ServeHTTP(rec, req) + + targets := reWebsocket.FindAllString(rec.Body.String(), -1) + require.True(t, len(targets) > 0) + body := targets[0] + require.Equal(t, body, tt.want) + }) + } +} diff --git a/contribs/gnodev/pkg/emitter/static/hotreload.js b/contribs/gnodev/pkg/emitter/static/hotreload.js index aabad4f341c..28e47c1ea15 100644 --- a/contribs/gnodev/pkg/emitter/static/hotreload.js +++ b/contribs/gnodev/pkg/emitter/static/hotreload.js @@ -1,6 +1,8 @@ (function() { // Define the events that will trigger a page reload - const eventsReload = {{ .ReloadEvents | json }}; + const eventsReload = [ + {{range .ReloadEvents}}'{{.}}',{{end}} + ]; // Establish the WebSocket connection to the event server const ws = new WebSocket('ws://{{- .Remote -}}'); From 7e21e23e8d496c6731b355ca5c39201578c23b3c Mon Sep 17 00:00:00 2001 From: Morgan Date: Mon, 20 Jan 2025 09:49:13 +0100 Subject: [PATCH 02/12] chore(github-bot): avoid references from bot comment to meta issue (#3551) --- contribs/github-bot/internal/check/comment.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contribs/github-bot/internal/check/comment.tmpl b/contribs/github-bot/internal/check/comment.tmpl index d9b633a69d5..acdb2681b92 100644 --- a/contribs/github-bot/internal/check/comment.tmpl +++ b/contribs/github-bot/internal/check/comment.tmpl @@ -27,7 +27,7 @@ 1. Complete manual checks for the PR, including the guidelines and additional checks if applicable. ##### 📚 Resources: -- [Report a bug with the bot](https://github.com/gnolang/gno/issues/3238). +- [Report a bug with the bot](https://www.github.com/gnolang/gno/issues/3238). - [View the bot’s configuration file](https://github.com/gnolang/gno/tree/master/contribs/github-bot/internal/config/config.go). {{ if or .AutoRules .ManualRules }}
Debug
From 5448754ad3fa442a1040d76e2694b07fc4a67f80 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Mon, 20 Jan 2025 10:28:47 +0100 Subject: [PATCH 03/12] feat: add r/demo/atomicswap (#2510) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - [x] implement - [x] unit test / txtar - [x] question: ugnot or grc20 or both? Maybe it’s time to encourage using `wugnot`. **-> both, with a callback mechanism.** - [x] question: p+r or just r? **-> just `r`, and a single file. let's do it more!** - [x] make the API gnokey compatible + add Render. Depends on #3397 (cherry-picked) Depends on #2529 (cherry-picked) Depends on #2549 (cherry-picked) Depends on #2551 Closes #2549 --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> Co-authored-by: Leon Hudak <33522493+leohhhn@users.noreply.github.com> --- .../gno.land/r/demo/atomicswap/atomicswap.gno | 175 +++++++ .../r/demo/atomicswap/atomicswap_test.gno | 434 ++++++++++++++++++ examples/gno.land/r/demo/atomicswap/gno.mod | 1 + examples/gno.land/r/demo/atomicswap/swap.gno | 98 ++++ .../pkg/integration/testdata/atomicswap.txtar | 41 ++ 5 files changed, 749 insertions(+) create mode 100644 examples/gno.land/r/demo/atomicswap/atomicswap.gno create mode 100644 examples/gno.land/r/demo/atomicswap/atomicswap_test.gno create mode 100644 examples/gno.land/r/demo/atomicswap/gno.mod create mode 100644 examples/gno.land/r/demo/atomicswap/swap.gno create mode 100644 gno.land/pkg/integration/testdata/atomicswap.txtar diff --git a/examples/gno.land/r/demo/atomicswap/atomicswap.gno b/examples/gno.land/r/demo/atomicswap/atomicswap.gno new file mode 100644 index 00000000000..8862feb1bed --- /dev/null +++ b/examples/gno.land/r/demo/atomicswap/atomicswap.gno @@ -0,0 +1,175 @@ +// Package atomicswap implements a hash time-locked contract (HTLC) for atomic swaps +// between native coins (ugnot) or GRC20 tokens. +// +// An atomic swap allows two parties to exchange assets in a trustless way, where +// either both transfers happen or neither does. The process works as follows: +// +// 1. Alice wants to swap with Bob. She generates a secret and creates a swap with +// Bob's address and the hash of the secret (hashlock). +// +// 2. Bob can claim the assets by providing the correct secret before the timelock expires. +// The secret proves Bob knows the preimage of the hashlock. +// +// 3. If Bob doesn't claim in time, Alice can refund the assets back to herself. +// +// Example usage for native coins: +// +// // Alice creates a swap with 1000ugnot for Bob +// secret := "mysecret" +// hashlock := hex.EncodeToString(sha256.Sum256([]byte(secret))) +// id, _ := atomicswap.NewCoinSwap(bobAddr, hashlock) // -send 1000ugnot +// +// // Bob claims the swap by providing the secret +// atomicswap.Claim(id, "mysecret") +// +// Example usage for GRC20 tokens: +// +// // Alice approves the swap contract to spend her tokens +// token.Approve(swapAddr, 1000) +// +// // Alice creates a swap with 1000 tokens for Bob +// id, _ := atomicswap.NewGRC20Swap(bobAddr, hashlock, "gno.land/r/demo/token") +// +// // Bob claims the swap by providing the secret +// atomicswap.Claim(id, "mysecret") +// +// If Bob doesn't claim in time (default 1 week), Alice can refund: +// +// atomicswap.Refund(id) +package atomicswap + +import ( + "std" + "strconv" + "time" + + "gno.land/p/demo/avl" + "gno.land/p/demo/grc/grc20" + "gno.land/p/demo/ufmt" + "gno.land/r/demo/grc20reg" +) + +const defaultTimelockDuration = 7 * 24 * time.Hour // 1w + +var ( + swaps avl.Tree // id -> *Swap + counter int +) + +// NewCoinSwap creates a new atomic swap contract for native coins. +// It uses a default timelock duration. +func NewCoinSwap(recipient std.Address, hashlock string) (int, *Swap) { + timelock := time.Now().Add(defaultTimelockDuration) + return NewCustomCoinSwap(recipient, hashlock, timelock) +} + +// NewGRC20Swap creates a new atomic swap contract for grc20 tokens. +// It uses gno.land/r/demo/grc20reg to lookup for a registered token. +func NewGRC20Swap(recipient std.Address, hashlock string, tokenRegistryKey string) (int, *Swap) { + timelock := time.Now().Add(defaultTimelockDuration) + tokenGetter := grc20reg.MustGet(tokenRegistryKey) + token := tokenGetter() + return NewCustomGRC20Swap(recipient, hashlock, timelock, token) +} + +// NewCoinSwapWithTimelock creates a new atomic swap contract for native coin. +// It allows specifying a custom timelock duration. +// It is not callable with `gnokey maketx call`, but can be imported by another contract or `gnokey maketx run`. +func NewCustomCoinSwap(recipient std.Address, hashlock string, timelock time.Time) (int, *Swap) { + sender := std.PrevRealm().Addr() + sent := std.GetOrigSend() + require(len(sent) != 0, "at least one coin needs to be sent") + + // Create the swap + sendFn := func(to std.Address) { + banker := std.GetBanker(std.BankerTypeRealmSend) + pkgAddr := std.GetOrigPkgAddr() + banker.SendCoins(pkgAddr, to, sent) + } + amountStr := sent.String() + swap := newSwap(sender, recipient, hashlock, timelock, amountStr, sendFn) + + counter++ + id := strconv.Itoa(counter) + swaps.Set(id, swap) + return counter, swap +} + +// NewCustomGRC20Swap creates a new atomic swap contract for grc20 tokens. +// It is not callable with `gnokey maketx call`, but can be imported by another contract or `gnokey maketx run`. +func NewCustomGRC20Swap(recipient std.Address, hashlock string, timelock time.Time, token *grc20.Token) (int, *Swap) { + sender := std.PrevRealm().Addr() + curAddr := std.CurrentRealm().Addr() + + allowance := token.Allowance(sender, curAddr) + require(allowance > 0, "no allowance") + + userTeller := token.CallerTeller() + err := userTeller.TransferFrom(sender, curAddr, allowance) + require(err == nil, "cannot retrieve tokens from allowance") + + amountStr := ufmt.Sprintf("%d%s", allowance, token.GetSymbol()) + sendFn := func(to std.Address) { + err := userTeller.Transfer(to, allowance) + require(err == nil, "cannot transfer tokens") + } + + swap := newSwap(sender, recipient, hashlock, timelock, amountStr, sendFn) + + counter++ + id := strconv.Itoa(counter) + swaps.Set(id, swap) + + return counter, swap +} + +// Claim loads a registered swap and tries to claim it. +func Claim(id int, secret string) { + swap := mustGet(id) + swap.Claim(secret) +} + +// Refund loads a registered swap and tries to refund it. +func Refund(id int) { + swap := mustGet(id) + swap.Refund() +} + +// Render returns a list of swaps (simplified) for the homepage, and swap details when specifying a swap ID. +func Render(path string) string { + if path == "" { // home + output := "" + size := swaps.Size() + max := 10 + swaps.ReverseIterateByOffset(size-max, max, func(key string, value interface{}) bool { + swap := value.(*Swap) + output += ufmt.Sprintf("- %s: %s -(%s)> %s - %s\n", + key, swap.sender, swap.amountStr, swap.recipient, swap.Status()) + return false + }) + return output + } else { // by id + swap, ok := swaps.Get(path) + if !ok { + return "404" + } + return swap.(*Swap).String() + } +} + +// require checks a condition and panics with a message if the condition is false. +func require(check bool, msg string) { + if !check { + panic(msg) + } +} + +// mustGet retrieves a swap by its id or panics. +func mustGet(id int) *Swap { + key := strconv.Itoa(id) + swap, ok := swaps.Get(key) + if !ok { + panic("unknown swap ID") + } + return swap.(*Swap) +} diff --git a/examples/gno.land/r/demo/atomicswap/atomicswap_test.gno b/examples/gno.land/r/demo/atomicswap/atomicswap_test.gno new file mode 100644 index 00000000000..0bcf6a1342d --- /dev/null +++ b/examples/gno.land/r/demo/atomicswap/atomicswap_test.gno @@ -0,0 +1,434 @@ +package atomicswap + +import ( + "crypto/sha256" + "encoding/hex" + "std" + "testing" + "time" + + "gno.land/p/demo/avl" + "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" + "gno.land/r/demo/tests/test20" +) + +var testRun bool + +func TestNewCustomCoinSwap_Claim(t *testing.T) { + t.Skip("skipping due to bad support for unit-test driven banker") + defer resetTestState() + + // Setup + sender := testutils.TestAddress("sender1") + recipient := testutils.TestAddress("recipient1") + amount := std.Coins{{Denom: "ugnot", Amount: 1}} + hashlock := sha256.Sum256([]byte("secret")) + hashlockHex := hex.EncodeToString(hashlock[:]) + timelock := time.Now().Add(1 * time.Hour) + + // Create a new swap + std.TestSetRealm(std.NewUserRealm(sender)) + std.TestSetOrigSend(amount, nil) + id, swap := NewCustomCoinSwap(recipient, hashlockHex, timelock) + uassert.Equal(t, 1, id) + + expected := `- status: active +- sender: g1wdjkuer9wgc47h6lta047h6lta047h6l56jtjc +- recipient: g1wfjkx6tsd9jkuap3ta047h6lta047h6lkk20gv +- amount: 1ugnot +- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b +- timelock: 2009-02-14T00:31:30Z +- remaining: 1h0m0s` + uassert.Equal(t, expected, swap.String()) + uassert.Equal(t, expected, Render("1")) + + // Test initial state + uassert.Equal(t, sender, swap.sender, "expected sender to match") + uassert.Equal(t, recipient, swap.recipient, "expected recipient to match") + uassert.Equal(t, swap.amountStr, amount.String(), "expected amount to match") + uassert.Equal(t, hashlockHex, swap.hashlock, "expected hashlock to match") + uassert.True(t, swap.timelock.Equal(timelock), "expected timelock to match") + uassert.False(t, swap.claimed, "expected claimed to be false") + uassert.False(t, swap.refunded, "expected refunded to be false") + + // Test claim + std.TestSetRealm(std.NewUserRealm(recipient)) + uassert.PanicsWithMessage(t, "invalid preimage", func() { swap.Claim("invalid") }) + swap.Claim("secret") + uassert.True(t, swap.claimed, "expected claimed to be true") + + // Test refund (should fail because already claimed) + uassert.PanicsWithMessage(t, "already claimed", swap.Refund) + uassert.PanicsWithMessage(t, "already claimed", func() { swap.Claim("secret") }) + + expected = `- status: claimed +- sender: g1wdjkuer9wgc47h6lta047h6lta047h6l56jtjc +- recipient: g1wfjkx6tsd9jkuap3ta047h6lta047h6lkk20gv +- amount: 1ugnot +- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b +- timelock: 2009-02-14T00:31:30Z +- remaining: 1h0m0s` + uassert.Equal(t, expected, swap.String()) + uassert.Equal(t, expected, Render("1")) +} + +func TestNewCustomCoinSwap_Refund(t *testing.T) { + defer resetTestState() + + // Setup + sender := testutils.TestAddress("sender2") + recipient := testutils.TestAddress("recipient2") + amount := std.Coins{{Denom: "ugnot", Amount: 1}} + hashlock := sha256.Sum256([]byte("secret")) + hashlockHex := hex.EncodeToString(hashlock[:]) + timelock := time.Now().Add(1 * time.Hour) + + // Create a new swap + std.TestSetRealm(std.NewUserRealm(sender)) + std.TestSetOrigSend(amount, nil) + id, swap := NewCustomCoinSwap(recipient, hashlockHex, timelock) // Create a new swap + uassert.Equal(t, 1, id) + + expected := `- status: active +- sender: g1wdjkuer9wge97h6lta047h6lta047h6ltfacad +- recipient: g1wfjkx6tsd9jkuapjta047h6lta047h6lducc3v +- amount: 1ugnot +- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b +- timelock: 2009-02-14T00:31:30Z +- remaining: 1h0m0s` + uassert.Equal(t, expected, swap.String()) + uassert.Equal(t, expected, Render("1")) + + // Test Refund + pkgAddr := std.DerivePkgAddr("gno.land/r/demo/atomicswap") + std.TestSetOrigPkgAddr(pkgAddr) + std.TestIssueCoins(pkgAddr, std.Coins{{"ugnot", 100000000}}) + uassert.PanicsWithMessage(t, "timelock not expired", swap.Refund) + swap.timelock = time.Now().Add(-1 * time.Hour) // override timelock + swap.Refund() + uassert.True(t, swap.refunded, "expected refunded to be true") + expected = `- status: refunded +- sender: g1wdjkuer9wge97h6lta047h6lta047h6ltfacad +- recipient: g1wfjkx6tsd9jkuapjta047h6lta047h6lducc3v +- amount: 1ugnot +- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b +- timelock: 2009-02-13T22:31:30Z +- remaining: 0s` + uassert.Equal(t, expected, swap.String()) + uassert.Equal(t, expected, Render("1")) +} + +func TestNewCustomGRC20Swap_Claim(t *testing.T) { + defer resetTestState() + + // Setup + sender := testutils.TestAddress("sender3") + recipient := testutils.TestAddress("recipient3") + rlm := std.DerivePkgAddr("gno.land/r/demo/atomicswap") + hashlock := sha256.Sum256([]byte("secret")) + hashlockHex := hex.EncodeToString(hashlock[:]) + timelock := time.Now().Add(1 * time.Hour) + + test20.PrivateLedger.Mint(sender, 100_000) + test20.PrivateLedger.Approve(sender, rlm, 70_000) + + // Create a new swap + std.TestSetRealm(std.NewUserRealm(sender)) + id, swap := NewCustomGRC20Swap(recipient, hashlockHex, timelock, test20.Token) + uassert.Equal(t, 1, id) + + expected := `- status: active +- sender: g1wdjkuer9wge47h6lta047h6lta047h6l5rk38l +- recipient: g1wfjkx6tsd9jkuapnta047h6lta047h6ly6k4pv +- amount: 70000TST +- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b +- timelock: 2009-02-14T00:31:30Z +- remaining: 1h0m0s` + uassert.Equal(t, expected, swap.String()) + uassert.Equal(t, expected, Render("1")) + + // Test initial state + uassert.Equal(t, sender, swap.sender, "expected sender to match") + uassert.Equal(t, recipient, swap.recipient, "expected recipient to match") + bal := test20.Token.BalanceOf(sender) + uassert.Equal(t, bal, uint64(30_000)) + bal = test20.Token.BalanceOf(rlm) + uassert.Equal(t, bal, uint64(70_000)) + bal = test20.Token.BalanceOf(recipient) + uassert.Equal(t, bal, uint64(0)) + + // uassert.Equal(t, swap.amountStr, amount.String(), "expected amount to match") + uassert.Equal(t, hashlockHex, swap.hashlock, "expected hashlock to match") + uassert.True(t, swap.timelock.Equal(timelock), "expected timelock to match") + uassert.False(t, swap.claimed, "expected claimed to be false") + uassert.False(t, swap.refunded, "expected refunded to be false") + + // Test claim + std.TestSetRealm(std.NewUserRealm(recipient)) + uassert.PanicsWithMessage(t, "invalid preimage", func() { swap.Claim("invalid") }) + swap.Claim("secret") + uassert.True(t, swap.claimed, "expected claimed to be true") + + bal = test20.Token.BalanceOf(sender) + uassert.Equal(t, bal, uint64(30_000)) + bal = test20.Token.BalanceOf(rlm) + uassert.Equal(t, bal, uint64(0)) + bal = test20.Token.BalanceOf(recipient) + uassert.Equal(t, bal, uint64(70_000)) + + // Test refund (should fail because already claimed) + uassert.PanicsWithMessage(t, "already claimed", swap.Refund) + uassert.PanicsWithMessage(t, "already claimed", func() { swap.Claim("secret") }) + + expected = `- status: claimed +- sender: g1wdjkuer9wge47h6lta047h6lta047h6l5rk38l +- recipient: g1wfjkx6tsd9jkuapnta047h6lta047h6ly6k4pv +- amount: 70000TST +- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b +- timelock: 2009-02-14T00:31:30Z +- remaining: 1h0m0s` + uassert.Equal(t, expected, swap.String()) + uassert.Equal(t, expected, Render("1")) +} + +func TestNewCustomGRC20Swap_Refund(t *testing.T) { + defer resetTestState() + + // Setup + sender := testutils.TestAddress("sender5") + recipient := testutils.TestAddress("recipient5") + rlm := std.DerivePkgAddr("gno.land/r/demo/atomicswap") + hashlock := sha256.Sum256([]byte("secret")) + hashlockHex := hex.EncodeToString(hashlock[:]) + timelock := time.Now().Add(1 * time.Hour) + + test20.PrivateLedger.Mint(sender, 100_000) + test20.PrivateLedger.Approve(sender, rlm, 70_000) + + // Create a new swap + std.TestSetRealm(std.NewUserRealm(sender)) + id, swap := NewCustomGRC20Swap(recipient, hashlockHex, timelock, test20.Token) + uassert.Equal(t, 1, id) + + expected := `- status: active +- sender: g1wdjkuer9wg647h6lta047h6lta047h6l5p6k3k +- recipient: g1wfjkx6tsd9jkuap4ta047h6lta047h6lmwmj6v +- amount: 70000TST +- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b +- timelock: 2009-02-14T00:31:30Z +- remaining: 1h0m0s` + uassert.Equal(t, expected, swap.String()) + uassert.Equal(t, expected, Render("1")) + + // Test initial state + uassert.Equal(t, sender, swap.sender, "expected sender to match") + uassert.Equal(t, recipient, swap.recipient, "expected recipient to match") + bal := test20.Token.BalanceOf(sender) + uassert.Equal(t, bal, uint64(30_000)) + bal = test20.Token.BalanceOf(rlm) + uassert.Equal(t, bal, uint64(70_000)) + bal = test20.Token.BalanceOf(recipient) + uassert.Equal(t, bal, uint64(0)) + + // Test Refund + pkgAddr := std.DerivePkgAddr("gno.land/r/demo/atomicswap") + std.TestSetOrigPkgAddr(pkgAddr) + std.TestIssueCoins(pkgAddr, std.Coins{{"ugnot", 100000000}}) + uassert.PanicsWithMessage(t, "timelock not expired", swap.Refund) + swap.timelock = time.Now().Add(-1 * time.Hour) // override timelock + swap.Refund() + uassert.True(t, swap.refunded, "expected refunded to be true") + + bal = test20.Token.BalanceOf(sender) + uassert.Equal(t, bal, uint64(100_000)) + bal = test20.Token.BalanceOf(rlm) + uassert.Equal(t, bal, uint64(0)) + bal = test20.Token.BalanceOf(recipient) + uassert.Equal(t, bal, uint64(0)) + + expected = `- status: refunded +- sender: g1wdjkuer9wg647h6lta047h6lta047h6l5p6k3k +- recipient: g1wfjkx6tsd9jkuap4ta047h6lta047h6lmwmj6v +- amount: 70000TST +- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b +- timelock: 2009-02-13T22:31:30Z +- remaining: 0s` + uassert.Equal(t, expected, swap.String()) + uassert.Equal(t, expected, Render("1")) +} + +func TestNewGRC20Swap_Claim(t *testing.T) { + defer resetTestState() + + // Setup + sender := testutils.TestAddress("sender4") + recipient := testutils.TestAddress("recipient4") + rlm := std.DerivePkgAddr("gno.land/r/demo/atomicswap") + hashlock := sha256.Sum256([]byte("secret")) + hashlockHex := hex.EncodeToString(hashlock[:]) + timelock := time.Now().Add(defaultTimelockDuration) + + test20.PrivateLedger.Mint(sender, 100_000) + test20.PrivateLedger.Approve(sender, rlm, 70_000) + + // Create a new swap + std.TestSetRealm(std.NewUserRealm(sender)) + id, swap := NewGRC20Swap(recipient, hashlockHex, "gno.land/r/demo/tests/test20") + uassert.Equal(t, 1, id) + + expected := `- status: active +- sender: g1wdjkuer9wg697h6lta047h6lta047h6ltt3lty +- recipient: g1wfjkx6tsd9jkuap5ta047h6lta047h6ljg4l2v +- amount: 70000TST +- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b +- timelock: 2009-02-20T23:31:30Z +- remaining: 168h0m0s` + uassert.Equal(t, expected, swap.String()) + uassert.Equal(t, expected, Render("1")) + + // Test initial state + uassert.Equal(t, sender, swap.sender, "expected sender to match") + uassert.Equal(t, recipient, swap.recipient, "expected recipient to match") + bal := test20.Token.BalanceOf(sender) + uassert.Equal(t, bal, uint64(30_000)) + bal = test20.Token.BalanceOf(rlm) + uassert.Equal(t, bal, uint64(70_000)) + bal = test20.Token.BalanceOf(recipient) + uassert.Equal(t, bal, uint64(0)) + + // uassert.Equal(t, swap.amountStr, amount.String(), "expected amount to match") + uassert.Equal(t, hashlockHex, swap.hashlock, "expected hashlock to match") + uassert.True(t, swap.timelock.Equal(timelock), "expected timelock to match") + uassert.False(t, swap.claimed, "expected claimed to be false") + uassert.False(t, swap.refunded, "expected refunded to be false") + + // Test claim + std.TestSetRealm(std.NewUserRealm(recipient)) + uassert.PanicsWithMessage(t, "invalid preimage", func() { swap.Claim("invalid") }) + swap.Claim("secret") + uassert.True(t, swap.claimed, "expected claimed to be true") + + bal = test20.Token.BalanceOf(sender) + uassert.Equal(t, bal, uint64(30_000)) + bal = test20.Token.BalanceOf(rlm) + uassert.Equal(t, bal, uint64(0)) + bal = test20.Token.BalanceOf(recipient) + uassert.Equal(t, bal, uint64(70_000)) + + // Test refund (should fail because already claimed) + uassert.PanicsWithMessage(t, "already claimed", swap.Refund) + uassert.PanicsWithMessage(t, "already claimed", func() { swap.Claim("secret") }) + + expected = `- status: claimed +- sender: g1wdjkuer9wg697h6lta047h6lta047h6ltt3lty +- recipient: g1wfjkx6tsd9jkuap5ta047h6lta047h6ljg4l2v +- amount: 70000TST +- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b +- timelock: 2009-02-20T23:31:30Z +- remaining: 168h0m0s` + uassert.Equal(t, expected, swap.String()) + uassert.Equal(t, expected, Render("1")) +} + +func TestNewGRC20Swap_Refund(t *testing.T) { + defer resetTestState() + + // Setup + sender := testutils.TestAddress("sender6") + recipient := testutils.TestAddress("recipient6") + rlm := std.DerivePkgAddr("gno.land/r/demo/atomicswap") + hashlock := sha256.Sum256([]byte("secret")) + hashlockHex := hex.EncodeToString(hashlock[:]) + timelock := time.Now().Add(defaultTimelockDuration) + + test20.PrivateLedger.Mint(sender, 100_000) + test20.PrivateLedger.Approve(sender, rlm, 70_000) + + // Create a new swap + std.TestSetRealm(std.NewUserRealm(sender)) + id, swap := NewGRC20Swap(recipient, hashlockHex, "gno.land/r/demo/tests/test20") + uassert.Equal(t, 1, id) + + expected := `- status: active +- sender: g1wdjkuer9wgm97h6lta047h6lta047h6ltj497r +- recipient: g1wfjkx6tsd9jkuapkta047h6lta047h6lqyf9rv +- amount: 70000TST +- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b +- timelock: 2009-02-20T23:31:30Z +- remaining: 168h0m0s` + uassert.Equal(t, expected, swap.String()) + uassert.Equal(t, expected, Render("1")) + + // Test initial state + uassert.Equal(t, sender, swap.sender, "expected sender to match") + uassert.Equal(t, recipient, swap.recipient, "expected recipient to match") + bal := test20.Token.BalanceOf(sender) + uassert.Equal(t, bal, uint64(30_000)) + bal = test20.Token.BalanceOf(rlm) + uassert.Equal(t, bal, uint64(70_000)) + bal = test20.Token.BalanceOf(recipient) + uassert.Equal(t, bal, uint64(0)) + + // Test Refund + pkgAddr := std.DerivePkgAddr("gno.land/r/demo/atomicswap") + std.TestSetOrigPkgAddr(pkgAddr) + std.TestIssueCoins(pkgAddr, std.Coins{{"ugnot", 100000000}}) + uassert.PanicsWithMessage(t, "timelock not expired", swap.Refund) + swap.timelock = time.Now().Add(-1 * time.Hour) // override timelock + swap.Refund() + uassert.True(t, swap.refunded, "expected refunded to be true") + + bal = test20.Token.BalanceOf(sender) + uassert.Equal(t, bal, uint64(100_000)) + bal = test20.Token.BalanceOf(rlm) + uassert.Equal(t, bal, uint64(0)) + bal = test20.Token.BalanceOf(recipient) + uassert.Equal(t, bal, uint64(0)) + + expected = `- status: refunded +- sender: g1wdjkuer9wgm97h6lta047h6lta047h6ltj497r +- recipient: g1wfjkx6tsd9jkuapkta047h6lta047h6lqyf9rv +- amount: 70000TST +- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b +- timelock: 2009-02-13T22:31:30Z +- remaining: 0s` + uassert.Equal(t, expected, swap.String()) + uassert.Equal(t, expected, Render("1")) +} + +func TestRender(t *testing.T) { + defer resetTestState() + + // Setup + alice := testutils.TestAddress("alice") + bob := testutils.TestAddress("bob") + charly := testutils.TestAddress("charly") + rlm := std.DerivePkgAddr("gno.land/r/demo/atomicswap") + hashlock := sha256.Sum256([]byte("secret")) + hashlockHex := hex.EncodeToString(hashlock[:]) + timelock := time.Now().Add(1 * time.Hour) + + test20.PrivateLedger.Mint(alice, 100_000) + std.TestSetRealm(std.NewUserRealm(alice)) + + userTeller := test20.Token.CallerTeller() + userTeller.Approve(rlm, 10_000) + _, bobSwap := NewCustomGRC20Swap(bob, hashlockHex, timelock, test20.Token) + + userTeller.Approve(rlm, 20_000) + _, _ = NewCustomGRC20Swap(charly, hashlockHex, timelock, test20.Token) + + std.TestSetRealm(std.NewUserRealm(bob)) + bobSwap.Claim("secret") + + expected := `- 2: g1v9kxjcm9ta047h6lta047h6lta047h6lzd40gh -(20000TST)> g1vd5xzunv09047h6lta047h6lta047h6lhsyveh - active +- 1: g1v9kxjcm9ta047h6lta047h6lta047h6lzd40gh -(10000TST)> g1vfhkyh6lta047h6lta047h6lta047h6l03vdhu - claimed +` + uassert.Equal(t, expected, Render("")) +} + +func resetTestState() { + swaps = avl.Tree{} + counter = 0 +} diff --git a/examples/gno.land/r/demo/atomicswap/gno.mod b/examples/gno.land/r/demo/atomicswap/gno.mod new file mode 100644 index 00000000000..1d6580c51e8 --- /dev/null +++ b/examples/gno.land/r/demo/atomicswap/gno.mod @@ -0,0 +1 @@ +module gno.land/r/demo/atomicswap diff --git a/examples/gno.land/r/demo/atomicswap/swap.gno b/examples/gno.land/r/demo/atomicswap/swap.gno new file mode 100644 index 00000000000..da40805221e --- /dev/null +++ b/examples/gno.land/r/demo/atomicswap/swap.gno @@ -0,0 +1,98 @@ +package atomicswap + +import ( + "crypto/sha256" + "encoding/hex" + "std" + "time" + + "gno.land/p/demo/ufmt" +) + +// Swap represents an atomic swap contract. +type Swap struct { + sender std.Address + recipient std.Address + hashlock string + timelock time.Time + claimed bool + refunded bool + amountStr string + sendFn func(to std.Address) +} + +func newSwap( + sender std.Address, + recipient std.Address, + hashlock string, + timelock time.Time, + amountStr string, + sendFn func(std.Address), +) *Swap { + require(time.Now().Before(timelock), "timelock must be in the future") + require(hashlock != "", "hashlock must not be empty") + return &Swap{ + recipient: recipient, + sender: sender, + hashlock: hashlock, + timelock: timelock, + claimed: false, + refunded: false, + sendFn: sendFn, + amountStr: amountStr, + } +} + +// Claim allows the recipient to claim the funds if they provide the correct preimage. +func (s *Swap) Claim(preimage string) { + require(!s.claimed, "already claimed") + require(!s.refunded, "already refunded") + require(std.PrevRealm().Addr() == s.recipient, "unauthorized") + + hashlock := sha256.Sum256([]byte(preimage)) + hashlockHex := hex.EncodeToString(hashlock[:]) + require(hashlockHex == s.hashlock, "invalid preimage") + + s.claimed = true + s.sendFn(s.recipient) +} + +// Refund allows the sender to refund the funds after the timelock has expired. +func (s *Swap) Refund() { + require(!s.claimed, "already claimed") + require(!s.refunded, "already refunded") + require(std.PrevRealm().Addr() == s.sender, "unauthorized") + require(time.Now().After(s.timelock), "timelock not expired") + + s.refunded = true + s.sendFn(s.sender) +} + +func (s Swap) Status() string { + switch { + case s.refunded: + return "refunded" + case s.claimed: + return "claimed" + case s.TimeRemaining() < 0: + return "expired" + default: + return "active" + } +} + +func (s Swap) TimeRemaining() time.Duration { + remaining := time.Until(s.timelock) + if remaining < 0 { + return 0 + } + return remaining +} + +// String returns the current state of the swap. +func (s Swap) String() string { + return ufmt.Sprintf( + "- status: %s\n- sender: %s\n- recipient: %s\n- amount: %s\n- hashlock: %s\n- timelock: %s\n- remaining: %s", + s.Status(), s.sender, s.recipient, s.amountStr, s.hashlock, s.timelock.Format(time.RFC3339), s.TimeRemaining().String(), + ) +} diff --git a/gno.land/pkg/integration/testdata/atomicswap.txtar b/gno.land/pkg/integration/testdata/atomicswap.txtar new file mode 100644 index 00000000000..6d0f89a6dc8 --- /dev/null +++ b/gno.land/pkg/integration/testdata/atomicswap.txtar @@ -0,0 +1,41 @@ +loadpkg gno.land/r/demo/atomicswap +# XXX: would be nice to specify an artibrary initial balance as an "adduser" argument. cc @gfanton +adduser test2 +adduser test3 + +gnoland start + +gnokey maketx send -send 1000000000ugnot -to $test2_user_addr -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid tendermint_test test1 +gnokey maketx send -send 1000000000ugnot -to $test3_user_addr -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid tendermint_test test1 + +gnokey query auth/accounts/$test2_user_addr +stdout 'coins.*:.*1010000000ugnot' + +gnokey query auth/accounts/$test3_user_addr +stdout 'coins.*:.*1010000000ugnot' + +# To generate the hash for "secret", use a hashing tool or library of your choice. For example: +# In Unix-based systems, you can use: +# echo -n "secret" | sha256sum +# This will produce a hashlock string like "2bb808d537b1da3e38bd30361aa85586dbbeacdd7126fef6a25ef97b5f27a25b". +# Replace the hashlock argument in the command below with the generated hash. +gnokey maketx call -pkgpath gno.land/r/demo/atomicswap -func NewCoinSwap -gas-fee 1000000ugnot -send 12345ugnot -gas-wanted 10000000 -args $test3_user_addr -args '2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b' -broadcast -chainid=tendermint_test test2 +stdout '(1 int)' +stdout ".*$test2_user_addr.*$test3_user_addr.*12345ugnot.*" +stdout 'OK!' + +gnokey maketx call -pkgpath gno.land/r/demo/atomicswap -func Render -gas-fee 1000000ugnot -gas-wanted 10000000 -args '' -broadcast -chainid=tendermint_test test2 +stdout 'OK!' + +gnokey query auth/accounts/$test2_user_addr +stdout 'coins.*:.*1007987655ugnot' +gnokey query auth/accounts/$test3_user_addr +stdout 'coins.*:.*1010000000ugnot' + +gnokey maketx call -pkgpath gno.land/r/demo/atomicswap -func Claim -gas-fee 1ugnot -gas-wanted 10000000 -args '1' -args 'secret' -broadcast -chainid=tendermint_test test3 +stdout 'OK!' + +gnokey query auth/accounts/$test2_user_addr +stdout 'coins.*:.*1007987655ugnot' +gnokey query auth/accounts/$test3_user_addr +stdout 'coins.*:.*1010012344ugnot' From a52b8b28ec56c98082759537935ee291d6d1571a Mon Sep 17 00:00:00 2001 From: Guilhem Fanton <8671905+gfanton@users.noreply.github.com> Date: Mon, 20 Jan 2025 12:14:47 +0100 Subject: [PATCH 04/12] chore: cleanup gno files (#3527) This PR mostly removes some unused variables and type assertions that should have been identified by `golang` as error at compile time. These errors were flagged by `gnopls`, and I'm unsure why they are not currently detected as error by our current `gno` tools suite. --------- Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> Co-authored-by: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Co-authored-by: Morgan Bazalgette --- examples/gno.land/p/demo/btree/btree_test.gno | 134 +++++++++--------- .../gno.land/p/demo/context/context_test.gno | 2 +- .../grc/grc1155/basic_grc1155_token_test.gno | 2 +- .../p/demo/grc/grc721/basic_nft_test.gno | 4 +- .../p/demo/grc/grc721/grc721_royalty_test.gno | 2 +- .../gno.land/p/demo/grc/grc777/dummy_test.gno | 2 +- .../subscription/recurring/recurring_test.gno | 2 +- .../p/moul/typeutil/typeutil_test.gno | 4 - examples/gno.land/p/n2p5/loci/loci_test.gno | 5 - .../p/wyhaines/rand/isaac/isaac_test.gno | 2 +- .../p/wyhaines/rand/isaac64/isaac64_test.gno | 2 +- .../gno.land/r/demo/foo1155/foo1155_test.gno | 3 +- examples/gno.land/r/demo/foo20/foo20_test.gno | 1 - .../gno.land/r/demo/foo721/foo721_test.gno | 2 +- .../games/dice_roller/dice_roller_test.gno | 2 +- .../r/demo/keystore/keystore_test.gno | 2 +- .../gno.land/r/gnoland/blog/gnoblog_test.gno | 3 - examples/gno.land/r/ursulovic/home/home.gno | 1 - 18 files changed, 81 insertions(+), 94 deletions(-) diff --git a/examples/gno.land/p/demo/btree/btree_test.gno b/examples/gno.land/p/demo/btree/btree_test.gno index a0f7c1c55ca..871e8c25e1d 100644 --- a/examples/gno.land/p/demo/btree/btree_test.gno +++ b/examples/gno.land/p/demo/btree/btree_test.gno @@ -138,7 +138,7 @@ func TestHas(t *testing.T) { } func TestMin(t *testing.T) { - min := Content(genericSeeding(New(WithDegree(10)), 53).Min()) + min := genericSeeding(New(WithDegree(10)), 53).Min().(Content) if min.Key != 0 { t.Errorf("Minimum should have been 0, but it was reported as %d.", min) @@ -146,24 +146,24 @@ func TestMin(t *testing.T) { } func TestMax(t *testing.T) { - max := Content(genericSeeding(New(WithDegree(10)), 53).Min()) + max := genericSeeding(New(WithDegree(10)), 53).Max().(Content) - if max.Key != 0 { - t.Errorf("Minimum should have been 0, but it was reported as %d.", max) + if max.Key != 52 { + t.Errorf("Maximum should have been 52, but it was reported as %d.", max) } } func TestGet(t *testing.T) { tree := genericSeeding(New(WithDegree(10)), 40) - if Content(tree.Get(Content{Key: 7})).Value != "Value_7" { - t.Errorf("Get(7) should have returned 'Value_7', but it returned %v.", tree.Get(Content{Key: 7})) + if val := tree.Get(Content{Key: 7}); val != nil && val.(Content).Value != "Value_7" { + t.Errorf("Get(7) should have returned 'Value_7', but it returned %v.", val) } - if Content(tree.Get(Content{Key: 39})).Value != "Value_39" { - t.Errorf("Get(40) should have returnd 'Value_39', but it returned %v.", tree.Get(Content{Key: 39})) + if val := tree.Get(Content{Key: 39}); val != nil && val.(Content).Value != "Value_39" { + t.Errorf("Get(39) should have returned 'Value_39', but it returned %v.", val) } - if tree.Get(Content{Key: 1111}) != nil { - t.Errorf("Get(1111) returned %v, but it should be nil.", Content(tree.Get(Content{Key: 1111}))) + if val := tree.Get(Content{Key: 1111}); val != nil { + t.Errorf("Get(1111) returned %v, but it should be nil.", val) } } @@ -174,8 +174,8 @@ func TestDescend(t *testing.T) { found := []int{} tree.Descend(func(_record Record) bool { - record := Content(_record) - found = append(found, int(record.Key)) + record := _record.(Content) + found = append(found, record.Key.(int)) return true }) @@ -191,8 +191,8 @@ func TestDescendGreaterThan(t *testing.T) { found := []int{} tree.DescendGreaterThan(Content{Key: 4}, func(_record Record) bool { - record := Content(_record) - found = append(found, int(record.Key)) + record := _record.(Content) + found = append(found, record.Key.(int)) return true }) @@ -208,8 +208,8 @@ func TestDescendLessOrEqual(t *testing.T) { found := []int{} tree.DescendLessOrEqual(Content{Key: 4}, func(_record Record) bool { - record := Content(_record) - found = append(found, int(record.Key)) + record := _record.(Content) + found = append(found, record.Key.(int)) return true }) @@ -225,8 +225,8 @@ func TestDescendRange(t *testing.T) { found := []int{} tree.DescendRange(Content{Key: 6}, Content{Key: 1}, func(_record Record) bool { - record := Content(_record) - found = append(found, int(record.Key)) + record := _record.(Content) + found = append(found, record.Key.(int)) return true }) @@ -242,8 +242,8 @@ func TestAscend(t *testing.T) { found := []int{} tree.Ascend(func(_record Record) bool { - record := Content(_record) - found = append(found, int(record.Key)) + record := _record.(Content) + found = append(found, record.Key.(int)) return true }) @@ -259,8 +259,8 @@ func TestAscendGreaterOrEqual(t *testing.T) { found := []int{} tree.AscendGreaterOrEqual(Content{Key: 5}, func(_record Record) bool { - record := Content(_record) - found = append(found, int(record.Key)) + record := _record.(Content) + found = append(found, record.Key.(int)) return true }) @@ -276,13 +276,13 @@ func TestAscendLessThan(t *testing.T) { found := []int{} tree.AscendLessThan(Content{Key: 5}, func(_record Record) bool { - record := Content(_record) - found = append(found, int(record.Key)) + record := _record.(Content) + found = append(found, record.Key.(int)) return true }) if intSlicesCompare(expected, found) != 0 { - t.Errorf("DescendLessOrEqual returned the wrong sequence. Expected %v, but got %v.", expected, found) + t.Errorf("AscendLessThan returned the wrong sequence. Expected %v, but got %v.", expected, found) } } @@ -293,13 +293,13 @@ func TestAscendRange(t *testing.T) { found := []int{} tree.AscendRange(Content{Key: 2}, Content{Key: 7}, func(_record Record) bool { - record := Content(_record) - found = append(found, int(record.Key)) + record := _record.(Content) + found = append(found, record.Key.(int)) return true }) if intSlicesCompare(expected, found) != 0 { - t.Errorf("DescendRange returned the wrong sequence. Expected %v, but got %v.", expected, found) + t.Errorf("AscendRange returned the wrong sequence. Expected %v, but got %v.", expected, found) } } @@ -309,11 +309,11 @@ func TestDeleteMin(t *testing.T) { expected := []int{0, 1, 2, 3, 4} found := []int{} - found = append(found, int(Content(tree.DeleteMin()).Key)) - found = append(found, int(Content(tree.DeleteMin()).Key)) - found = append(found, int(Content(tree.DeleteMin()).Key)) - found = append(found, int(Content(tree.DeleteMin()).Key)) - found = append(found, int(Content(tree.DeleteMin()).Key)) + found = append(found, tree.DeleteMin().(Content).Key.(int)) + found = append(found, tree.DeleteMin().(Content).Key.(int)) + found = append(found, tree.DeleteMin().(Content).Key.(int)) + found = append(found, tree.DeleteMin().(Content).Key.(int)) + found = append(found, tree.DeleteMin().(Content).Key.(int)) if intSlicesCompare(expected, found) != 0 { t.Errorf("5 rounds of DeleteMin returned the wrong elements. Expected %v, but got %v.", expected, found) @@ -326,11 +326,11 @@ func TestShift(t *testing.T) { expected := []int{0, 1, 2, 3, 4} found := []int{} - found = append(found, int(Content(tree.Shift()).Key)) - found = append(found, int(Content(tree.Shift()).Key)) - found = append(found, int(Content(tree.Shift()).Key)) - found = append(found, int(Content(tree.Shift()).Key)) - found = append(found, int(Content(tree.Shift()).Key)) + found = append(found, tree.Shift().(Content).Key.(int)) + found = append(found, tree.Shift().(Content).Key.(int)) + found = append(found, tree.Shift().(Content).Key.(int)) + found = append(found, tree.Shift().(Content).Key.(int)) + found = append(found, tree.Shift().(Content).Key.(int)) if intSlicesCompare(expected, found) != 0 { t.Errorf("5 rounds of Shift returned the wrong elements. Expected %v, but got %v.", expected, found) @@ -343,14 +343,14 @@ func TestDeleteMax(t *testing.T) { expected := []int{99, 98, 97, 96, 95} found := []int{} - found = append(found, int(Content(tree.DeleteMax()).Key)) - found = append(found, int(Content(tree.DeleteMax()).Key)) - found = append(found, int(Content(tree.DeleteMax()).Key)) - found = append(found, int(Content(tree.DeleteMax()).Key)) - found = append(found, int(Content(tree.DeleteMax()).Key)) + found = append(found, tree.DeleteMax().(Content).Key.(int)) + found = append(found, tree.DeleteMax().(Content).Key.(int)) + found = append(found, tree.DeleteMax().(Content).Key.(int)) + found = append(found, tree.DeleteMax().(Content).Key.(int)) + found = append(found, tree.DeleteMax().(Content).Key.(int)) if intSlicesCompare(expected, found) != 0 { - t.Errorf("5 rounds of DeleteMin returned the wrong elements. Expected %v, but got %v.", expected, found) + t.Errorf("5 rounds of DeleteMax returned the wrong elements. Expected %v, but got %v.", expected, found) } } @@ -360,14 +360,14 @@ func TestPop(t *testing.T) { expected := []int{99, 98, 97, 96, 95} found := []int{} - found = append(found, int(Content(tree.Pop()).Key)) - found = append(found, int(Content(tree.Pop()).Key)) - found = append(found, int(Content(tree.Pop()).Key)) - found = append(found, int(Content(tree.Pop()).Key)) - found = append(found, int(Content(tree.Pop()).Key)) + found = append(found, tree.Pop().(Content).Key.(int)) + found = append(found, tree.Pop().(Content).Key.(int)) + found = append(found, tree.Pop().(Content).Key.(int)) + found = append(found, tree.Pop().(Content).Key.(int)) + found = append(found, tree.Pop().(Content).Key.(int)) if intSlicesCompare(expected, found) != 0 { - t.Errorf("5 rounds of DeleteMin returned the wrong elements. Expected %v, but got %v.", expected, found) + t.Errorf("5 rounds of Pop returned the wrong elements. Expected %v, but got %v.", expected, found) } } @@ -383,13 +383,15 @@ func TestInsertGet(t *testing.T) { } for count := 0; count < 20; count++ { - if tree.Get(Content{Key: count}) != expected[count] { - t.Errorf("Insert/Get doesn't appear to be working. Expected to retrieve %v with key %d, but got %v.", expected[count], count, tree.Get(Content{Key: count})) + val := tree.Get(Content{Key: count}) + if val == nil || val.(Content) != expected[count] { + t.Errorf("Insert/Get doesn't appear to be working. Expected to retrieve %v with key %d, but got %v.", expected[count], count, val) } } } func TestClone(t *testing.T) { + // Implement the clone test } // ***** The following tests are functional or stress testing type tests. @@ -440,18 +442,18 @@ func TestBTree(t *testing.T) { val := tree.Get(test.test) if test.expected { - if val != nil && Content(val).Value == test.test.Value { + if val != nil && val.(Content).Value == test.test.Value { t.Logf("Found expected key:value %v:%v", test.test.Key, test.test.Value) } else { if val == nil { t.Logf("Didn't find %v, but expected", test.test.Key) } else { - t.Errorf("Expected key %v:%v, but found %v:%v.", test.test.Key, test.test.Value, Content(val).Key, Content(val).Value) + t.Errorf("Expected key %v:%v, but found %v:%v.", test.test.Key, test.test.Value, val.(Content).Key, val.(Content).Value) } } } else { if val != nil { - t.Errorf("Did not expect key %v, but found key:value %v:%v", test.test.Key, Content(val).Key, Content(val).Value) + t.Errorf("Did not expect key %v, but found key:value %v:%v", test.test.Key, val.(Content).Key, val.(Content).Value) } else { t.Logf("Didn't find %v, but wasn't expected", test.test.Key) } @@ -474,7 +476,7 @@ func TestBTree(t *testing.T) { t.Logf("Tree Length: %d", tree.Len()) tree.Ascend(func(_record Record) bool { - record := Content(_record) + record := _record.(Content) t.Logf("Key:Value == %v:%v", record.Key, record.Value) if record.Key != sortedInsertData[pos].Key { t.Errorf("Out of order! Expected %v, but got %v", sortedInsertData[pos].Key, record.Key) @@ -487,7 +489,7 @@ func TestBTree(t *testing.T) { pos = len(sortedInsertData) - 1 tree.Descend(func(_record Record) bool { - record := Content(_record) + record := _record.(Content) t.Logf("Key:Value == %v:%v", record.Key, record.Value) if record.Key != sortedInsertData[pos].Key { t.Errorf("Out of order! Expected %v, but got %v", sortedInsertData[pos].Key, record.Key) @@ -497,10 +499,10 @@ func TestBTree(t *testing.T) { }) deleteTests := []Content{ - Content{Key: 10, Value: "Value_10"}, - Content{Key: 15, Value: ""}, - Content{Key: "banana", Value: "Fruit_banana"}, - Content{Key: "kiwi", Value: ""}, + {Key: 10, Value: "Value_10"}, + {Key: 15, Value: ""}, + {Key: "banana", Value: "Fruit_banana"}, + {Key: "kiwi", Value: ""}, } for _, test := range deleteTests { fmt.Printf("\nDeleting %+v\n", test) @@ -514,7 +516,7 @@ func TestBTree(t *testing.T) { for _, test := range deleteTests { val := tree.Get(test) if val != nil { - t.Errorf("Did not expect key %v, but found key:value %v:%v", test.Key, Content(val).Key, Content(val).Value) + t.Errorf("Did not expect key %v, but found key:value %v:%v", test.Key, val.(Content).Key, val.(Content).Value) } else { t.Logf("Didn't find %v, but wasn't expected", test.Key) } @@ -558,7 +560,7 @@ func TestStress(t *testing.T) { val := tree.Get(content) if i%2 == 0 { if val != nil { - t.Errorf("Didn't expect key %v, but found key:value %v:%v", content.Key, Content(val).Key, Content(val).Value) + t.Errorf("Didn't expect key %v, but found key:value %v:%v", content.Key, val.(Content).Key, val.(Content).Value) } } else { if val == nil { @@ -643,7 +645,7 @@ func TestBTreeCloneIsolation(t *testing.T) { if i%3 == 0 || i%2 == 0 { if val != nil { - t.Errorf("Didn't expect key %v, but found key:value %v:%v", content.Key, Content(val).Key, Content(val).Value) + t.Errorf("Didn't expect key %v, but found key:value %v:%v", content.Key, val.(Content).Key, val.(Content).Value) } } else { if val == nil { @@ -654,7 +656,7 @@ func TestBTreeCloneIsolation(t *testing.T) { val = clone.Get(content) if i%2 != 0 || i%3 == 0 { if val != nil { - t.Errorf("Didn't expect key %v, but found key:value %v:%v", content.Key, Content(val).Key, Content(val).Value) + t.Errorf("Didn't expect key %v, but found key:value %v:%v", content.Key, val.(Content).Key, val.(Content).Value) } } else { if val == nil { @@ -665,7 +667,7 @@ func TestBTreeCloneIsolation(t *testing.T) { val = clone2.Get(content) if i%2 != 0 || (i-2)%3 == 0 { if val != nil { - t.Errorf("Didn't expect key %v, but found key:value %v:%v", content.Key, Content(val).Key, Content(val).Value) + t.Errorf("Didn't expect key %v, but found key:value %v:%v", content.Key, val.(Content).Key, val.(Content).Value) } } else { if val == nil { diff --git a/examples/gno.land/p/demo/context/context_test.gno b/examples/gno.land/p/demo/context/context_test.gno index 0059f0d2a25..d8e2069363d 100644 --- a/examples/gno.land/p/demo/context/context_test.gno +++ b/examples/gno.land/p/demo/context/context_test.gno @@ -9,7 +9,7 @@ func TestContextExample(t *testing.T) { ctx := WithValue(Empty(), k, "Gno") if v := ctx.Value(k); v != nil { - if string(v) != "Gno" { + if v.(string) != "Gno" { t.Errorf("language value should be Gno, but is %s", v) } } else { diff --git a/examples/gno.land/p/demo/grc/grc1155/basic_grc1155_token_test.gno b/examples/gno.land/p/demo/grc/grc1155/basic_grc1155_token_test.gno index 2fef3431b43..930ae03fa04 100644 --- a/examples/gno.land/p/demo/grc/grc1155/basic_grc1155_token_test.gno +++ b/examples/gno.land/p/demo/grc/grc1155/basic_grc1155_token_test.gno @@ -30,7 +30,7 @@ func TestBalanceOf(t *testing.T) { tid1 := TokenID("1") tid2 := TokenID("2") - balanceZeroAddressOfToken1, err := dummy.BalanceOf(zeroAddress, tid1) + _, err := dummy.BalanceOf(zeroAddress, tid1) uassert.Error(t, err, "should result in error") balanceAddr1OfToken1, err := dummy.BalanceOf(addr1, tid1) diff --git a/examples/gno.land/p/demo/grc/grc721/basic_nft_test.gno b/examples/gno.land/p/demo/grc/grc721/basic_nft_test.gno index 6375b0307a8..0481bf6b268 100644 --- a/examples/gno.land/p/demo/grc/grc721/basic_nft_test.gno +++ b/examples/gno.land/p/demo/grc/grc721/basic_nft_test.gno @@ -128,7 +128,7 @@ func TestGetApproved(t *testing.T) { dummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol) uassert.True(t, dummy != nil, "should not be nil") - approvedAddr, err := dummy.GetApproved(TokenID("invalid")) + _, err := dummy.GetApproved(TokenID("invalid")) uassert.Error(t, err, "should result in error") } @@ -247,7 +247,7 @@ func TestBurn(t *testing.T) { uassert.NoError(t, err, "should not result in error") // Check Owner of Token id - owner, err := dummy.OwnerOf(TokenID("1")) + _, err = dummy.OwnerOf(TokenID("1")) uassert.Error(t, err, "should result in error") } diff --git a/examples/gno.land/p/demo/grc/grc721/grc721_royalty_test.gno b/examples/gno.land/p/demo/grc/grc721/grc721_royalty_test.gno index 7893453a1c6..af5b4d3b239 100644 --- a/examples/gno.land/p/demo/grc/grc721/grc721_royalty_test.gno +++ b/examples/gno.land/p/demo/grc/grc721/grc721_royalty_test.gno @@ -32,7 +32,7 @@ func TestSetTokenRoyalty(t *testing.T) { uassert.NoError(t, derr, "Should not result in error") // Test case: Invalid token ID - err := dummy.SetTokenRoyalty(TokenID("3"), RoyaltyInfo{ + _ = dummy.SetTokenRoyalty(TokenID("3"), RoyaltyInfo{ PaymentAddress: paymentAddress, Percentage: percentage, }) diff --git a/examples/gno.land/p/demo/grc/grc777/dummy_test.gno b/examples/gno.land/p/demo/grc/grc777/dummy_test.gno index 5f8ac3598d1..33415fc689a 100644 --- a/examples/gno.land/p/demo/grc/grc777/dummy_test.gno +++ b/examples/gno.land/p/demo/grc/grc777/dummy_test.gno @@ -11,7 +11,7 @@ type dummyImpl struct{} var _ IGRC777 = (*dummyImpl)(nil) func TestInterface(t *testing.T) { - var dummy IGRC777 = &dummyImpl{} + var _ IGRC777 = &dummyImpl{} } func (impl *dummyImpl) GetName() string { panic("not implemented") } diff --git a/examples/gno.land/p/demo/subscription/recurring/recurring_test.gno b/examples/gno.land/p/demo/subscription/recurring/recurring_test.gno index e8bca15c0bf..0b458b716ec 100644 --- a/examples/gno.land/p/demo/subscription/recurring/recurring_test.gno +++ b/examples/gno.land/p/demo/subscription/recurring/recurring_test.gno @@ -26,7 +26,7 @@ func TestRecurringSubscription(t *testing.T) { err = rs.HasValidSubscription(std.PrevRealm().Addr()) uassert.NoError(t, err, "Expected Alice to have access") - expiration, err := rs.GetExpiration(std.PrevRealm().Addr()) + _, err = rs.GetExpiration(std.PrevRealm().Addr()) uassert.NoError(t, err, "Expected to get expiration for Alice") } diff --git a/examples/gno.land/p/moul/typeutil/typeutil_test.gno b/examples/gno.land/p/moul/typeutil/typeutil_test.gno index 543ea1deec4..2aaf1b0e93d 100644 --- a/examples/gno.land/p/moul/typeutil/typeutil_test.gno +++ b/examples/gno.land/p/moul/typeutil/typeutil_test.gno @@ -278,10 +278,6 @@ func TestIsZero(t *testing.T) { } func TestToInterfaceSlice(t *testing.T) { - now := time.Now() - addr := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") - str := testStringer{value: "hello"} - tests := []struct { name string input interface{} diff --git a/examples/gno.land/p/n2p5/loci/loci_test.gno b/examples/gno.land/p/n2p5/loci/loci_test.gno index bb216a8539e..6df6d4f9b2d 100644 --- a/examples/gno.land/p/n2p5/loci/loci_test.gno +++ b/examples/gno.land/p/n2p5/loci/loci_test.gno @@ -8,11 +8,6 @@ import ( ) func TestLociStore(t *testing.T) { - t.Parallel() - - u1 := testutils.TestAddress("u1") - u2 := testutils.TestAddress("u1") - t.Run("TestSet", func(t *testing.T) { t.Parallel() store := New() diff --git a/examples/gno.land/p/wyhaines/rand/isaac/isaac_test.gno b/examples/gno.land/p/wyhaines/rand/isaac/isaac_test.gno index b08621e271c..fdf633bb543 100644 --- a/examples/gno.land/p/wyhaines/rand/isaac/isaac_test.gno +++ b/examples/gno.land/p/wyhaines/rand/isaac/isaac_test.gno @@ -14,7 +14,7 @@ type OpenISAAC struct { } func TestISAACSeeding(t *testing.T) { - isaac := New() + _ = New() } func TestISAACRand(t *testing.T) { diff --git a/examples/gno.land/p/wyhaines/rand/isaac64/isaac64_test.gno b/examples/gno.land/p/wyhaines/rand/isaac64/isaac64_test.gno index 239e7f818fb..74abf2c13ca 100644 --- a/examples/gno.land/p/wyhaines/rand/isaac64/isaac64_test.gno +++ b/examples/gno.land/p/wyhaines/rand/isaac64/isaac64_test.gno @@ -14,7 +14,7 @@ type OpenISAAC struct { } func TestISAACSeeding(t *testing.T) { - isaac := New() + _ = New() } func TestISAACRand(t *testing.T) { diff --git a/examples/gno.land/r/demo/foo1155/foo1155_test.gno b/examples/gno.land/r/demo/foo1155/foo1155_test.gno index 64d4bc1256f..8ea722939f2 100644 --- a/examples/gno.land/r/demo/foo1155/foo1155_test.gno +++ b/examples/gno.land/r/demo/foo1155/foo1155_test.gno @@ -11,9 +11,8 @@ func TestFoo721(t *testing.T) { admin := users.AddressOrName("g10x5phu0k6p64cwrhfpsc8tk43st9kug6wft530") bob := users.AddressOrName("g1ze6et22ces5atv79y4xh38s4kuraey4y2fr6tw") tid1 := grc1155.TokenID("1") - tid2 := grc1155.TokenID("2") - for i, tc := range []struct { + for _, tc := range []struct { name string expected interface{} fn func() interface{} diff --git a/examples/gno.land/r/demo/foo20/foo20_test.gno b/examples/gno.land/r/demo/foo20/foo20_test.gno index b9e80fbb476..dbafe2d6c1d 100644 --- a/examples/gno.land/r/demo/foo20/foo20_test.gno +++ b/examples/gno.land/r/demo/foo20/foo20_test.gno @@ -61,7 +61,6 @@ func TestReadOnlyPublicMethods(t *testing.T) { func TestErrConditions(t *testing.T) { var ( admin = pusers.AddressOrName("g1manfred47kzduec920z88wfr64ylksmdcedlf5") - alice = pusers.AddressOrName(testutils.TestAddress("alice")) empty = pusers.AddressOrName("") ) diff --git a/examples/gno.land/r/demo/foo721/foo721_test.gno b/examples/gno.land/r/demo/foo721/foo721_test.gno index 2477ca34fde..fab39e561d1 100644 --- a/examples/gno.land/r/demo/foo721/foo721_test.gno +++ b/examples/gno.land/r/demo/foo721/foo721_test.gno @@ -13,7 +13,7 @@ func TestFoo721(t *testing.T) { admin := pusers.AddressOrName("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") hariom := pusers.AddressOrName("g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm") - for i, tc := range []struct { + for _, tc := range []struct { name string expected interface{} fn func() interface{} diff --git a/examples/gno.land/r/demo/games/dice_roller/dice_roller_test.gno b/examples/gno.land/r/demo/games/dice_roller/dice_roller_test.gno index 2f6770a366f..4697b03bf66 100644 --- a/examples/gno.land/r/demo/games/dice_roller/dice_roller_test.gno +++ b/examples/gno.land/r/demo/games/dice_roller/dice_roller_test.gno @@ -128,7 +128,7 @@ func TestPlayBeyondGameEnd(t *testing.T) { Play(gameID) // Check if the game is over - g, err := getGame(gameID) + _, err := getGame(gameID) urequire.NoError(t, err) // Attempt to play more should fail diff --git a/examples/gno.land/r/demo/keystore/keystore_test.gno b/examples/gno.land/r/demo/keystore/keystore_test.gno index 9b5fafa2f95..03b61e79663 100644 --- a/examples/gno.land/r/demo/keystore/keystore_test.gno +++ b/examples/gno.land/r/demo/keystore/keystore_test.gno @@ -56,7 +56,7 @@ func TestRender(t *testing.T) { p := "" if len(tc.ps) > 0 { p = tc.owner.String() - for i, psv := range tc.ps { + for _, psv := range tc.ps { p += ":" + psv } } diff --git a/examples/gno.land/r/gnoland/blog/gnoblog_test.gno b/examples/gno.land/r/gnoland/blog/gnoblog_test.gno index b4658db4fb5..d046eb80f42 100644 --- a/examples/gno.land/r/gnoland/blog/gnoblog_test.gno +++ b/examples/gno.land/r/gnoland/blog/gnoblog_test.gno @@ -8,9 +8,6 @@ import ( func TestPackage(t *testing.T) { std.TestSetOrigCaller(std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5")) - - author := std.GetOrigCaller() - // by default, no posts. { got := Render("") diff --git a/examples/gno.land/r/ursulovic/home/home.gno b/examples/gno.land/r/ursulovic/home/home.gno index c03d8a66868..cc420df5e6e 100644 --- a/examples/gno.land/r/ursulovic/home/home.gno +++ b/examples/gno.land/r/ursulovic/home/home.gno @@ -19,7 +19,6 @@ var ( githubUrl string linkedinUrl string - connectUrl string imageUpdatePrice int64 isValidUrl func(string) bool From b1352ac42d753d7acca11297419fcef559561125 Mon Sep 17 00:00:00 2001 From: Petar Dambovaliev Date: Mon, 20 Jan 2025 12:58:54 +0100 Subject: [PATCH 05/12] fix: global var dependencies (#2077) fixes [this](https://github.com/gnolang/gno/issues/1463) by adding missing logic in in `findUndefined2` --------- Co-authored-by: jaekwon --- gnovm/pkg/gnolang/nodes.go | 1 + gnovm/pkg/gnolang/preprocess.go | 570 ++++++++++++++++++++++++++++++-- gnovm/tests/files/closure.gno | 16 + gnovm/tests/files/var27.gno | 80 +++++ gnovm/tests/files/var28.gno | 18 + 5 files changed, 649 insertions(+), 36 deletions(-) create mode 100644 gnovm/tests/files/closure.gno create mode 100644 gnovm/tests/files/var27.gno create mode 100644 gnovm/tests/files/var28.gno diff --git a/gnovm/pkg/gnolang/nodes.go b/gnovm/pkg/gnolang/nodes.go index b85d1ac7026..0f1f4388623 100644 --- a/gnovm/pkg/gnolang/nodes.go +++ b/gnovm/pkg/gnolang/nodes.go @@ -157,6 +157,7 @@ const ( ATTR_LOOP_USES GnoAttribute = "ATTR_LOOP_USES" // []Name loop defines actually used. ATTR_SHIFT_RHS GnoAttribute = "ATTR_SHIFT_RHS" ATTR_LAST_BLOCK_STMT GnoAttribute = "ATTR_LAST_BLOCK_STMT" + ATTR_GLOBAL GnoAttribute = "ATTR_GLOBAL" ) type Attributes struct { diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index ffa0f518331..96e09642cd8 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -129,6 +129,7 @@ func PredefineFileSet(store Store, pn *PackageNode, fset *FileSet) { continue } + d.SetAttribute(ATTR_GLOBAL, true) // recursively predefine dependencies. d2, _ := predefineNow(store, fn, d) @@ -516,6 +517,17 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { if cd, ok := d.(*ValueDecl); ok { checkValDefineMismatch(cd) } + + isGlobal := true + + for i := len(ns) - 1; i > 0; i-- { + if _, ok := ns[i].(*FuncDecl); ok { + isGlobal = false + } + } + + d.SetAttribute(ATTR_GLOBAL, isGlobal) + // recursively predefine dependencies. d2, ppd := predefineNow(store, last, d) if ppd { @@ -3779,16 +3791,486 @@ func assertTypeDeclNoCycle2(store Store, last BlockNode, x Expr, stack *[]Name, // type expressions, which must get preprocessed for inner // composite type eliding to work. func findUndefined(store Store, last BlockNode, x Expr) (un Name) { - return findUndefined2(store, last, x, nil) + return findUndefined2(store, last, x, nil, true) +} + +// finds the next undefined identifier and returns it if it is global +func findUndefined2SkipLocals(store Store, last BlockNode, x Expr, t Type) Name { + name := findUndefinedGlobal(store, last, x, t) + + if name == "" { + return "" + } + + existsLocal := func(name Name, bn BlockNode) bool { + curr := bn + for { + currNames := curr.GetBlockNames() + + for _, currName := range currNames { + if currName == name { + return true + } + } + + newcurr := bn.GetStaticBlock().GetParentNode(store) + + if curr == newcurr { + return false + } + + curr = newcurr + + if curr == nil { + return false + } + + _, isFile := curr.(*FileNode) + + if isFile { + return false + } + } + } + + pkg := packageOf(last) + + if _, _, ok := pkg.FileSet.GetDeclForSafe(name); !ok { + return "" + } + + isLocal := existsLocal(name, last) + + if isLocal { + return "" + } + + return name +} + +func findUndefinedStmt(store Store, last BlockNode, stmt Stmt, t Type) Name { + switch s := stmt.(type) { + case *TypeDecl: + un := findUndefined2SkipLocals(store, last, s.Type, t) + + if un != "" { + return un + } + case *ValueDecl: + un := findUndefined2SkipLocals(store, last, s.Type, t) + + if un != "" { + return un + } + for _, rh := range s.Values { + un := findUndefined2SkipLocals(store, last, rh, t) + + if un != "" { + return un + } + } + case *DeclStmt: + for _, rh := range s.Body { + un := findUndefinedStmt(store, last, rh, t) + + if un != "" { + return un + } + } + case *IncDecStmt: + un := findUndefined2SkipLocals(store, last, s.X, t) + + if un != "" { + return un + } + case *PanicStmt: + un := findUndefined2SkipLocals(store, last, s.Exception, t) + + if un != "" { + return un + } + case *BlockStmt: + for _, rh := range s.Body { + un := findUndefinedStmt(store, s, rh, t) + + if un != "" { + return un + } + } + case *DeferStmt: + un := findUndefined2SkipLocals(store, last, s.Call.Func, t) + + if un != "" { + return un + } + + for _, rh := range s.Call.Args { + un = findUndefined2SkipLocals(store, last, rh, t) + + if un != "" { + return un + } + } + case *SwitchStmt: + un := findUndefined2SkipLocals(store, last, s.X, t) + if un != "" { + return un + } + + un = findUndefinedStmt(store, last, s.Init, t) + if un != "" { + return un + } + + for _, b := range s.Clauses { + b := b + un = findUndefinedStmt(store, s, &b, t) + + if un != "" { + return un + } + } + case *SwitchClauseStmt: + for _, rh := range s.Cases { + un := findUndefined2SkipLocals(store, last, rh, t) + + if un != "" { + return un + } + } + + for _, b := range s.Body { + un := findUndefinedStmt(store, last, b, t) + + if un != "" { + return un + } + } + + case *ExprStmt: + return findUndefined2SkipLocals(store, last, s.X, t) + case *AssignStmt: + for _, rh := range s.Rhs { + un := findUndefined2SkipLocals(store, last, rh, t) + + if un != "" { + return un + } + } + case *IfStmt: + un := findUndefinedStmt(store, last, s.Init, t) + if un != "" { + return un + } + + un = findUndefined2SkipLocals(store, last, s.Cond, t) + if un != "" { + return un + } + + un = findUndefinedStmt(store, last, &s.Else, t) + if un != "" { + return un + } + + un = findUndefinedStmt(store, last, &s.Then, t) + if un != "" { + return un + } + case *IfCaseStmt: + for _, b := range s.Body { + un := findUndefinedStmt(store, last, b, t) + + if un != "" { + return un + } + } + case *ReturnStmt: + for _, b := range s.Results { + un := findUndefined2SkipLocals(store, last, b, t) + if un != "" { + return un + } + } + case *RangeStmt: + un := findUndefined2SkipLocals(store, last, s.X, t) + if un != "" { + return un + } + + for _, b := range s.Body { + un := findUndefinedStmt(store, last, b, t) + if un != "" { + return un + } + } + case *ForStmt: + un := findUndefinedStmt(store, s, s.Init, t) + if un != "" { + return un + } + + un = findUndefined2SkipLocals(store, s, s.Cond, t) + if un != "" { + return un + } + + un = findUndefinedStmt(store, s, s.Post, t) + if un != "" { + return un + } + + for _, b := range s.Body { + un := findUndefinedStmt(store, last, b, t) + if un != "" { + return un + } + } + case *BranchStmt: + case nil: + return "" + default: + panic(fmt.Sprintf("findUndefinedStmt: %T not supported", s)) + } + return "" +} + +func getGlobalValueRef(sb BlockNode, store Store, n Name) *TypedValue { + sbb := sb.GetStaticBlock() + idx, ok := sb.GetLocalIndex(n) + bb := &sb.GetStaticBlock().Block + bp := sb.GetParentNode(store) + + for { + if ok && sbb.Types[idx] != nil && (bp == nil || bp.GetParentNode(store) == nil) { + return bb.GetPointerToInt(store, int(idx)).TV + } else if bp != nil { + idx, ok = bp.GetLocalIndex(n) + sbb = bp.GetStaticBlock() + bb = sbb.GetBlock() + bp = bp.GetParentNode(store) + } else { + return nil + } + } +} + +func findUndefinedGlobal(store Store, last BlockNode, x Expr, t Type) (un Name) { + if x == nil { + return + } + switch cx := x.(type) { + case *NameExpr: + if tv := getGlobalValueRef(last, store, cx.Name); tv != nil { + return + } + + if _, ok := UverseNode().GetLocalIndex(cx.Name); ok { + // XXX NOTE even if the name is shadowed by a file + // level declaration, it is fine to return here as it + // will be predefined later. + return + } + + return cx.Name + case *BasicLitExpr: + return + case *BinaryExpr: + un = findUndefinedGlobal(store, last, cx.Left, nil) + if un != "" { + return + } + un = findUndefinedGlobal(store, last, cx.Right, nil) + if un != "" { + return + } + case *SelectorExpr: + return findUndefinedGlobal(store, last, cx.X, nil) + case *SliceExpr: + un = findUndefinedGlobal(store, last, cx.X, nil) + if un != "" { + return + } + if cx.Low != nil { + un = findUndefinedGlobal(store, last, cx.Low, nil) + if un != "" { + return + } + } + if cx.High != nil { + un = findUndefinedGlobal(store, last, cx.High, nil) + if un != "" { + return + } + } + if cx.Max != nil { + un = findUndefinedGlobal(store, last, cx.Max, nil) + if un != "" { + return + } + } + case *StarExpr: + return findUndefinedGlobal(store, last, cx.X, nil) + case *RefExpr: + return findUndefinedGlobal(store, last, cx.X, nil) + case *TypeAssertExpr: + un = findUndefinedGlobal(store, last, cx.X, nil) + if un != "" { + return + } + return findUndefinedGlobal(store, last, cx.Type, nil) + case *UnaryExpr: + return findUndefinedGlobal(store, last, cx.X, nil) + case *CompositeLitExpr: + var ct Type + if cx.Type == nil { + if t == nil { + panic("cannot elide unknown composite type") + } + ct = t + cx.Type = constType(cx, t) + } else { + un = findUndefinedGlobal(store, last, cx.Type, nil) + if un != "" { + return + } + // preprocess now for eliding purposes. + // TODO recursive preprocessing here is hacky, find a better + // way. This cannot be done asynchronously, cuz undefined + // names ought to be returned immediately to let the caller + // predefine it. + cx.Type = Preprocess(store, last, cx.Type).(Expr) // recursive + ct = evalStaticType(store, last, cx.Type) + // elide composite lit element (nested) composite types. + elideCompositeElements(cx, ct) + } + switch ct.Kind() { + case ArrayKind, SliceKind, MapKind: + for _, kvx := range cx.Elts { + un = findUndefinedGlobal(store, last, kvx.Key, nil) + if un != "" { + return + } + un = findUndefinedGlobal(store, last, kvx.Value, ct.Elem()) + if un != "" { + return + } + } + case StructKind: + for _, kvx := range cx.Elts { + un = findUndefinedGlobal(store, last, kvx.Value, nil) + if un != "" { + return + } + } + default: + panic(fmt.Sprintf( + "unexpected composite lit type %s", + ct.String())) + } + case *FuncLitExpr: + for _, stmt := range cx.Body { + un = findUndefinedStmt(store, cx, stmt, t) + + if un != "" { + return + } + } + return findUndefinedGlobal(store, last, &cx.Type, nil) + case *FieldTypeExpr: + return findUndefinedGlobal(store, last, cx.Type, nil) + case *ArrayTypeExpr: + if cx.Len != nil { + un = findUndefinedGlobal(store, last, cx.Len, nil) + if un != "" { + return + } + } + return findUndefinedGlobal(store, last, cx.Elt, nil) + case *SliceTypeExpr: + return findUndefinedGlobal(store, last, cx.Elt, nil) + case *InterfaceTypeExpr: + for i := range cx.Methods { + un = findUndefinedGlobal(store, last, &cx.Methods[i], nil) + if un != "" { + return + } + } + case *ChanTypeExpr: + return findUndefinedGlobal(store, last, cx.Value, nil) + case *FuncTypeExpr: + for i := range cx.Params { + un = findUndefinedGlobal(store, last, &cx.Params[i], nil) + if un != "" { + return + } + } + for i := range cx.Results { + un = findUndefinedGlobal(store, last, &cx.Results[i], nil) + if un != "" { + return + } + } + case *MapTypeExpr: + un = findUndefinedGlobal(store, last, cx.Key, nil) + if un != "" { + return + } + un = findUndefinedGlobal(store, last, cx.Value, nil) + if un != "" { + return + } + case *StructTypeExpr: + for i := range cx.Fields { + un = findUndefinedGlobal(store, last, &cx.Fields[i], nil) + if un != "" { + return + } + } + case *MaybeNativeTypeExpr: + un = findUndefinedGlobal(store, last, cx.Type, nil) + if un != "" { + return + } + case *CallExpr: + un = findUndefinedGlobal(store, last, cx.Func, nil) + if un != "" { + return + } + for i := range cx.Args { + un = findUndefinedGlobal(store, last, cx.Args[i], nil) + if un != "" { + return + } + } + case *IndexExpr: + un = findUndefinedGlobal(store, last, cx.X, nil) + if un != "" { + return + } + un = findUndefinedGlobal(store, last, cx.Index, nil) + if un != "" { + return + } + case *constTypeExpr: + return + case *ConstExpr: + return + default: + panic(fmt.Sprintf( + "unexpected expr: %v (%v)", + x, reflect.TypeOf(x))) + } + return } -func findUndefined2(store Store, last BlockNode, x Expr, t Type) (un Name) { +func findUndefined2(store Store, last BlockNode, x Expr, t Type, skipPredefined bool) (un Name) { if x == nil { return } switch cx := x.(type) { case *NameExpr: - if tv := last.GetValueRef(store, cx.Name, true); tv != nil { + if tv := last.GetValueRef(store, cx.Name, skipPredefined); tv != nil { return } if _, ok := UverseNode().GetLocalIndex(cx.Name); ok { @@ -3801,51 +4283,51 @@ func findUndefined2(store Store, last BlockNode, x Expr, t Type) (un Name) { case *BasicLitExpr: return case *BinaryExpr: - un = findUndefined(store, last, cx.Left) + un = findUndefined2(store, last, cx.Left, nil, skipPredefined) if un != "" { return } - un = findUndefined(store, last, cx.Right) + un = findUndefined2(store, last, cx.Right, nil, skipPredefined) if un != "" { return } case *SelectorExpr: - return findUndefined(store, last, cx.X) + return findUndefined2(store, last, cx.X, nil, skipPredefined) case *SliceExpr: - un = findUndefined(store, last, cx.X) + un = findUndefined2(store, last, cx.X, nil, skipPredefined) if un != "" { return } if cx.Low != nil { - un = findUndefined(store, last, cx.Low) + un = findUndefined2(store, last, cx.Low, nil, skipPredefined) if un != "" { return } } if cx.High != nil { - un = findUndefined(store, last, cx.High) + un = findUndefined2(store, last, cx.High, nil, skipPredefined) if un != "" { return } } if cx.Max != nil { - un = findUndefined(store, last, cx.Max) + un = findUndefined2(store, last, cx.Max, nil, skipPredefined) if un != "" { return } } case *StarExpr: - return findUndefined(store, last, cx.X) + return findUndefined2(store, last, cx.X, nil, skipPredefined) case *RefExpr: - return findUndefined(store, last, cx.X) + return findUndefined2(store, last, cx.X, nil, skipPredefined) case *TypeAssertExpr: - un = findUndefined(store, last, cx.X) + un = findUndefined2(store, last, cx.X, nil, skipPredefined) if un != "" { return } - return findUndefined(store, last, cx.Type) + return findUndefined2(store, last, cx.Type, nil, skipPredefined) case *UnaryExpr: - return findUndefined(store, last, cx.X) + return findUndefined2(store, last, cx.X, nil, skipPredefined) case *CompositeLitExpr: var ct Type if cx.Type == nil { @@ -3855,7 +4337,7 @@ func findUndefined2(store Store, last BlockNode, x Expr, t Type) (un Name) { ct = t cx.Type = constType(cx, t) } else { - un = findUndefined(store, last, cx.Type) + un = findUndefined2(store, last, cx.Type, nil, skipPredefined) if un != "" { return } @@ -3872,18 +4354,18 @@ func findUndefined2(store Store, last BlockNode, x Expr, t Type) (un Name) { switch ct.Kind() { case ArrayKind, SliceKind, MapKind: for _, kvx := range cx.Elts { - un = findUndefined(store, last, kvx.Key) + un = findUndefined2(store, last, kvx.Key, nil, skipPredefined) if un != "" { return } - un = findUndefined2(store, last, kvx.Value, ct.Elem()) + un = findUndefined2(store, last, kvx.Value, ct.Elem(), skipPredefined) if un != "" { return } } case StructKind: for _, kvx := range cx.Elts { - un = findUndefined(store, last, kvx.Value) + un = findUndefined2(store, last, kvx.Value, nil, skipPredefined) if un != "" { return } @@ -3894,43 +4376,53 @@ func findUndefined2(store Store, last BlockNode, x Expr, t Type) (un Name) { ct.String())) } case *FuncLitExpr: - return findUndefined(store, last, &cx.Type) + if cx.GetAttribute(ATTR_GLOBAL) == true { + for _, stmt := range cx.Body { + un = findUndefinedStmt(store, cx, stmt, t) + + if un != "" { + return + } + } + } + + return findUndefined2(store, last, &cx.Type, nil, skipPredefined) case *FieldTypeExpr: - return findUndefined(store, last, cx.Type) + return findUndefined2(store, last, cx.Type, nil, skipPredefined) case *ArrayTypeExpr: if cx.Len != nil { - un = findUndefined(store, last, cx.Len) + un = findUndefined2(store, last, cx.Len, nil, skipPredefined) if un != "" { return } } - return findUndefined(store, last, cx.Elt) + return findUndefined2(store, last, cx.Elt, nil, skipPredefined) case *SliceTypeExpr: - return findUndefined(store, last, cx.Elt) + return findUndefined2(store, last, cx.Elt, nil, skipPredefined) case *InterfaceTypeExpr: for i := range cx.Methods { - un = findUndefined(store, last, &cx.Methods[i]) + un = findUndefined2(store, last, &cx.Methods[i], nil, skipPredefined) if un != "" { return } } case *ChanTypeExpr: - return findUndefined(store, last, cx.Value) + return findUndefined2(store, last, cx.Value, nil, skipPredefined) case *FuncTypeExpr: for i := range cx.Params { - un = findUndefined(store, last, &cx.Params[i]) + un = findUndefined2(store, last, &cx.Params[i], nil, skipPredefined) if un != "" { return } } for i := range cx.Results { - un = findUndefined(store, last, &cx.Results[i]) + un = findUndefined2(store, last, &cx.Results[i], nil, skipPredefined) if un != "" { return } } case *MapTypeExpr: - un = findUndefined(store, last, cx.Key) + un = findUndefined2(store, last, cx.Key, nil, skipPredefined) if un != "" { return } @@ -3940,33 +4432,34 @@ func findUndefined2(store Store, last BlockNode, x Expr, t Type) (un Name) { } case *StructTypeExpr: for i := range cx.Fields { - un = findUndefined(store, last, &cx.Fields[i]) + un = findUndefined2(store, last, &cx.Fields[i], nil, skipPredefined) if un != "" { return } } case *MaybeNativeTypeExpr: - un = findUndefined(store, last, cx.Type) + un = findUndefined2(store, last, cx.Type, nil, skipPredefined) if un != "" { return } case *CallExpr: - un = findUndefined(store, last, cx.Func) + cx.Func.SetAttribute(ATTR_GLOBAL, cx.GetAttribute(ATTR_GLOBAL)) + un = findUndefined2(store, last, cx.Func, nil, skipPredefined) if un != "" { return } for i := range cx.Args { - un = findUndefined(store, last, cx.Args[i]) + un = findUndefined2(store, last, cx.Args[i], nil, skipPredefined) if un != "" { return } } case *IndexExpr: - un = findUndefined(store, last, cx.X) + un = findUndefined2(store, last, cx.X, nil, skipPredefined) if un != "" { return } - un = findUndefined(store, last, cx.Index) + un = findUndefined2(store, last, cx.Index, nil, skipPredefined) if un != "" { return } @@ -4082,8 +4575,12 @@ func predefineNow2(store Store, last BlockNode, d Decl, stack *[]Name) (Decl, bo if !file.IsInitialized() { panic("all types from files in file-set should have already been predefined") } + + declaration := *decl + declaration.SetAttribute(ATTR_GLOBAL, true) + // predefine dependency (recursive). - *decl, _ = predefineNow2(store, file, *decl, stack) + *decl, _ = predefineNow2(store, file, declaration, stack) } else { break } @@ -4241,6 +4738,7 @@ func tryPredefine(store Store, last BlockNode, d Decl) (un Name) { return } for _, vx := range d.Values { + vx.SetAttribute(ATTR_GLOBAL, d.GetAttribute(ATTR_GLOBAL)) un = findUndefined(store, last, vx) if un != "" { return diff --git a/gnovm/tests/files/closure.gno b/gnovm/tests/files/closure.gno new file mode 100644 index 00000000000..ea05c025954 --- /dev/null +++ b/gnovm/tests/files/closure.gno @@ -0,0 +1,16 @@ +package main + +func main() { + +} + +var a = func() { + b() +} + +var b = func() { + a() +} + +// Error: +// main/files/closure.gno:7:5: constant definition loop with a diff --git a/gnovm/tests/files/var27.gno b/gnovm/tests/files/var27.gno new file mode 100644 index 00000000000..65ab9307b9b --- /dev/null +++ b/gnovm/tests/files/var27.gno @@ -0,0 +1,80 @@ +package main + +func main() {} + +var myDep string + +var myVar1 = func() { + a := myDep1 +} + +var myDep1 string + +var myVar2 = func() { + aaa := "" + + switch myDep { + case aaa: + println(myDep2) + } +} + +var myDep2 string + +var myVar3 = func() { + for _, c := range myDep3 { + println(c) + } +} + +var myDep3 string + +var v1 = func() int { + v2 := 11 + return v2 +}() + +var v2 = func() int { + return v1 +}() + +var v3 = func() int { + return func() int { + v4 := 11 + return v4 + }() +}() + +var v4 = func() int { + return v3 +}() + +var v5 = func() int { + v6 := 11 + return func() int { + return v6 + }() +}() + +var v6 = func() int { + return v5 +}() + +var other = func() { + if true { + something := 2 + print(something) // 2 + } else { + print(something) // a string, but single shared 'st' masks the outer/global reference. + } +} +var something = "a string" + +var other1 = func() { + if true { + something1 := 2 + print(something1) // 2 + } + print(something1) // a string, but single shared 'st' masks the outer/global reference. +} +var something1 = "a string" \ No newline at end of file diff --git a/gnovm/tests/files/var28.gno b/gnovm/tests/files/var28.gno new file mode 100644 index 00000000000..279f60168a4 --- /dev/null +++ b/gnovm/tests/files/var28.gno @@ -0,0 +1,18 @@ +package main + +func main() {} + +var foo = func() (bool, bool) { + return true, true +} + +var x = func() bool { return a }() +var a, b = foo() + +var a1 = func() int { + type B1 b1 + x := B1(1) + return int(x) +} + +type b1 int \ No newline at end of file From 09764ad0e5c6e7bc9671d27ead7ab8214b67e6c8 Mon Sep 17 00:00:00 2001 From: Emmanuel T Odeke Date: Mon, 20 Jan 2025 04:00:57 -0800 Subject: [PATCH 06/12] fix(tm2/pkg/bft,os,p2p): close leaking resources (#3556) This change closes some leaking resources that were identified in a preliminary code read. Fixes #3029 Fixes #3030 Fixes #3031 --- tm2/pkg/bft/privval/utils.go | 9 ++++++++- tm2/pkg/os/tempfile.go | 11 +++++++---- tm2/pkg/p2p/transport.go | 8 +++++++- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/tm2/pkg/bft/privval/utils.go b/tm2/pkg/bft/privval/utils.go index c759d9dde9d..a98a36ef04a 100644 --- a/tm2/pkg/bft/privval/utils.go +++ b/tm2/pkg/bft/privval/utils.go @@ -25,7 +25,7 @@ func IsConnTimeout(err error) bool { } // NewSignerListener creates a new SignerListenerEndpoint using the corresponding listen address -func NewSignerListener(listenAddr string, logger *slog.Logger) (*SignerListenerEndpoint, error) { +func NewSignerListener(listenAddr string, logger *slog.Logger) (_ *SignerListenerEndpoint, rerr error) { var listener net.Listener protocol, address := osm.ProtocolAndAddress(listenAddr) @@ -33,6 +33,13 @@ func NewSignerListener(listenAddr string, logger *slog.Logger) (*SignerListenerE if err != nil { return nil, err } + + defer func() { + if rerr != nil { + ln.Close() + } + }() + switch protocol { case "unix": listener = NewUnixListener(ln) diff --git a/tm2/pkg/os/tempfile.go b/tm2/pkg/os/tempfile.go index 277813051fd..46d97739b80 100644 --- a/tm2/pkg/os/tempfile.go +++ b/tm2/pkg/os/tempfile.go @@ -107,14 +107,17 @@ func WriteFileAtomic(filename string, data []byte, perm os.FileMode) (err error) } break } + + // Clean up in any case. + defer func() { + f.Close() + os.Remove(f.Name()) + }() + if i == atomicWriteFileMaxNumWriteAttempts { return fmt.Errorf("could not create atomic write file after %d attempts", i) } - // Clean up in any case. Defer stacking order is last-in-first-out. - defer os.Remove(f.Name()) - defer f.Close() - if n, err := f.Write(data); err != nil { return err } else if n < len(data) { diff --git a/tm2/pkg/p2p/transport.go b/tm2/pkg/p2p/transport.go index 9edef9a15e5..150072ad5eb 100644 --- a/tm2/pkg/p2p/transport.go +++ b/tm2/pkg/p2p/transport.go @@ -147,13 +147,19 @@ func (mt *MultiplexTransport) Close() error { } // Listen starts an active process of listening for incoming connections [NON-BLOCKING] -func (mt *MultiplexTransport) Listen(addr types.NetAddress) error { +func (mt *MultiplexTransport) Listen(addr types.NetAddress) (rerr error) { // Reserve a port, and start listening ln, err := net.Listen("tcp", addr.DialString()) if err != nil { return fmt.Errorf("unable to listen on address, %w", err) } + defer func() { + if rerr != nil { + ln.Close() + } + }() + if addr.Port == 0 { // net.Listen on port 0 means the kernel will auto-allocate a port // - find out which one has been given to us. From 52ddd005ef5f18a8f053e0b5284c8bbe6dfe3129 Mon Sep 17 00:00:00 2001 From: ltzmaxwell Date: Mon, 20 Jan 2025 20:52:59 +0800 Subject: [PATCH 07/12] fix(gnovm): correct map key persistence (#3127) # update: closes: https://github.com/gnolang/gno/issues/2060 and some other issues, by: The main issue is that after the pointer value is retrieved from the store, the `TV` field is not correctly filled, it leads to inconsistent map key calculation. this PR solves this problem. =========================================================== ~~closes: https://github.com/gnolang/gno/issues/2060 and some other issues, by:~~ ~~- correct map key computation when it's a pointer;~~ ~~- make key object owned by map value.~~ ~~The root cause of the issue lies in how the map key is calculated when using a pointer type. Currently, the memory address of the pointer value is used to generate the map key. However, this approach is inconsistent between the VM space and storage space. As a result, if a map key derived from a pointer is persisted and later restored, the two keys do not match.~~ ~~The proposed solution is to derive an immutable identifier for pointer values. This involves calculating an absolute path for the variables that the pointer value references, ensuring a corresponding and consistent `origin`.~~ ~~The absolute path consists of `pkgId:blockId:[index]`, providing a globally unique identifier for the pointer.~~ ~~For example: In the code below, the map key `i`, which is a pointer value, has an origin represented as: `15bc39c5756bbda67fd0d56bc9e86b29d7b444fc:1:[1]`~~ ~~// PKGPATH: gno.land/r/ptr_map~~ ~~package ptr_map~~ ~~var ( m = map[*int]int{} i = new(int) )~~ ~~func AddToMap(value int) { m[i] = value }~~ ~~func GetFromMap() int { return m[i] }~~ ~~func init() { *i = 1 AddToMap(5) }~~ ~~// ----above is initialized and persisted before main is executed.~~ ~~func main() { ~~r := GetFromMap() println(r == 5)~~ *i = 2 // this changes TV, also Base of a pointer value r = GetFromMap() println(r == 5)~~ }~~ ~~// Output:~~ ~~// true~~ ~~// true~~ --- .../pkg/integration/testdata/ptr_mapkey.txtar | 31 ++++++++++++++ gnovm/Makefile | 3 +- gnovm/pkg/gnolang/values.go | 1 + gnovm/tests/files/ptrmap_1.gno | 27 ++++++++++++ gnovm/tests/files/ptrmap_10.gno | 21 ++++++++++ gnovm/tests/files/ptrmap_11.gno | 22 ++++++++++ gnovm/tests/files/ptrmap_12.gno | 21 ++++++++++ gnovm/tests/files/ptrmap_13.gno | 22 ++++++++++ gnovm/tests/files/ptrmap_14.gno | 20 +++++++++ gnovm/tests/files/ptrmap_15.gno | 23 +++++++++++ gnovm/tests/files/ptrmap_16.gno | 24 +++++++++++ gnovm/tests/files/ptrmap_17.gno | 23 +++++++++++ gnovm/tests/files/ptrmap_18.gno | 33 +++++++++++++++ gnovm/tests/files/ptrmap_19.gno | 37 +++++++++++++++++ gnovm/tests/files/ptrmap_2.gno | 35 ++++++++++++++++ gnovm/tests/files/ptrmap_20.gno | 38 +++++++++++++++++ gnovm/tests/files/ptrmap_22.gno | 30 ++++++++++++++ gnovm/tests/files/ptrmap_23.gno | 17 ++++++++ gnovm/tests/files/ptrmap_24.gno | 28 +++++++++++++ gnovm/tests/files/ptrmap_25.gno | 34 +++++++++++++++ gnovm/tests/files/ptrmap_26.gno | 33 +++++++++++++++ gnovm/tests/files/ptrmap_3.gno | 34 +++++++++++++++ gnovm/tests/files/ptrmap_4.gno | 30 ++++++++++++++ gnovm/tests/files/ptrmap_5.gno | 28 +++++++++++++ gnovm/tests/files/ptrmap_6.gno | 37 +++++++++++++++++ gnovm/tests/files/ptrmap_7.gno | 41 +++++++++++++++++++ gnovm/tests/files/ptrmap_8.gno | 26 ++++++++++++ gnovm/tests/files/ptrmap_9.gno | 27 ++++++++++++ 28 files changed, 745 insertions(+), 1 deletion(-) create mode 100644 gno.land/pkg/integration/testdata/ptr_mapkey.txtar create mode 100644 gnovm/tests/files/ptrmap_1.gno create mode 100644 gnovm/tests/files/ptrmap_10.gno create mode 100644 gnovm/tests/files/ptrmap_11.gno create mode 100644 gnovm/tests/files/ptrmap_12.gno create mode 100644 gnovm/tests/files/ptrmap_13.gno create mode 100644 gnovm/tests/files/ptrmap_14.gno create mode 100644 gnovm/tests/files/ptrmap_15.gno create mode 100644 gnovm/tests/files/ptrmap_16.gno create mode 100644 gnovm/tests/files/ptrmap_17.gno create mode 100644 gnovm/tests/files/ptrmap_18.gno create mode 100644 gnovm/tests/files/ptrmap_19.gno create mode 100644 gnovm/tests/files/ptrmap_2.gno create mode 100644 gnovm/tests/files/ptrmap_20.gno create mode 100644 gnovm/tests/files/ptrmap_22.gno create mode 100644 gnovm/tests/files/ptrmap_23.gno create mode 100644 gnovm/tests/files/ptrmap_24.gno create mode 100644 gnovm/tests/files/ptrmap_25.gno create mode 100644 gnovm/tests/files/ptrmap_26.gno create mode 100644 gnovm/tests/files/ptrmap_3.gno create mode 100644 gnovm/tests/files/ptrmap_4.gno create mode 100644 gnovm/tests/files/ptrmap_5.gno create mode 100644 gnovm/tests/files/ptrmap_6.gno create mode 100644 gnovm/tests/files/ptrmap_7.gno create mode 100644 gnovm/tests/files/ptrmap_8.gno create mode 100644 gnovm/tests/files/ptrmap_9.gno diff --git a/gno.land/pkg/integration/testdata/ptr_mapkey.txtar b/gno.land/pkg/integration/testdata/ptr_mapkey.txtar new file mode 100644 index 00000000000..77f0f845c12 --- /dev/null +++ b/gno.land/pkg/integration/testdata/ptr_mapkey.txtar @@ -0,0 +1,31 @@ +gnoland start + +# add contract +gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/demo/ptrmap -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +stdout OK! + +gnokey maketx call -pkgpath gno.land/r/demo/ptrmap -func AddToMap -args 5 -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +stdout OK! + +gnokey maketx call -pkgpath gno.land/r/demo/ptrmap -func GetFromMap -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +stdout '(5 int)' +stdout OK! + +-- gno.mod -- +module gno.land/r/demo/ptrmap + +-- realm.gno -- +package ptrmap + +var ( + m = map[*int]int{} + i = new(int) +) + +func AddToMap(value int) { + m[i] = value +} + +func GetFromMap() int { + return m[i] +} \ No newline at end of file diff --git a/gnovm/Makefile b/gnovm/Makefile index d724ffbb6a2..babb7ad74ca 100644 --- a/gnovm/Makefile +++ b/gnovm/Makefile @@ -84,7 +84,7 @@ run.bench.storage: build.bench.storage ######################################## # Test suite .PHONY: test -test: _test.cmd _test.pkg _test.stdlibs +test: _test.filetest _test.cmd _test.pkg _test.stdlibs .PHONY: _test.cmd _test.cmd: @@ -116,6 +116,7 @@ _test.pkg: _test.stdlibs: go run ./cmd/gno test -v ./stdlibs/... +.PHONY: _test.filetest _test.filetest:; go test pkg/gnolang/files_test.go -test.short -run 'TestFiles$$/' $(GOTEST_FLAGS) diff --git a/gnovm/pkg/gnolang/values.go b/gnovm/pkg/gnolang/values.go index da887764c8e..3bfd7c0286f 100644 --- a/gnovm/pkg/gnolang/values.go +++ b/gnovm/pkg/gnolang/values.go @@ -1563,6 +1563,7 @@ func (tv *TypedValue) ComputeMapKey(store Store, omitType bool) MapKey { pbz := tv.PrimitiveBytes() bz = append(bz, pbz...) case *PointerType: + fillValueTV(store, tv) ptr := uintptr(unsafe.Pointer(tv.V.(PointerValue).TV)) bz = append(bz, uintptrToBytes(&ptr)...) case FieldType: diff --git a/gnovm/tests/files/ptrmap_1.gno b/gnovm/tests/files/ptrmap_1.gno new file mode 100644 index 00000000000..4e496a82f39 --- /dev/null +++ b/gnovm/tests/files/ptrmap_1.gno @@ -0,0 +1,27 @@ +// PKGPATH: gno.land/r/ptr_map +package ptr_map + +var ( + m = map[int]int{} + i = 0 +) + +func AddToMap(value int) { + m[i] = value +} + +func GetFromMap() int { + return m[i] +} + +func init() { + AddToMap(5) +} + +func main() { + r := GetFromMap() + println(r == 5) +} + +// Output: +// true diff --git a/gnovm/tests/files/ptrmap_10.gno b/gnovm/tests/files/ptrmap_10.gno new file mode 100644 index 00000000000..92f61f557cb --- /dev/null +++ b/gnovm/tests/files/ptrmap_10.gno @@ -0,0 +1,21 @@ +// PKGPATH: gno.land/r/ptr_map +package ptr_map + +var ( + m = map[*int]string{} + a, b = 1, 2 + arr = [2]*int{&a, &b} +) + +func init() { + m[arr[0]] = "first key" +} + +func main() { + println(m[arr[0]]) // Output: first key + println(m[arr[1]] == "") +} + +// Output: +// first key +// true diff --git a/gnovm/tests/files/ptrmap_11.gno b/gnovm/tests/files/ptrmap_11.gno new file mode 100644 index 00000000000..6ffeed62f0b --- /dev/null +++ b/gnovm/tests/files/ptrmap_11.gno @@ -0,0 +1,22 @@ +// PKGPATH: gno.land/r/ptr_map +package ptr_map + +var ( + m = map[*int]string{} + a, b = 1, 2 + S = []*int{&a, &b} // slice + index = 0 +) + +func init() { + m[S[index]] = "first key" +} + +func main() { + println(m[S[index]]) // Output: first key + println(m[S[1]] == "") +} + +// Output: +// first key +// true diff --git a/gnovm/tests/files/ptrmap_12.gno b/gnovm/tests/files/ptrmap_12.gno new file mode 100644 index 00000000000..b0360fb5cef --- /dev/null +++ b/gnovm/tests/files/ptrmap_12.gno @@ -0,0 +1,21 @@ +// PKGPATH: gno.land/r/ptr_map +package ptr_map + +var ( + m = map[*int]string{} + a, b = 1, 2 + S = []*int{&a, &b} +) + +func init() { + m[S[0]] = "first key" +} + +func main() { + println(m[S[0]]) // Output: first key + println(m[S[1]] == "") +} + +// Output: +// first key +// true diff --git a/gnovm/tests/files/ptrmap_13.gno b/gnovm/tests/files/ptrmap_13.gno new file mode 100644 index 00000000000..0e0d24c6865 --- /dev/null +++ b/gnovm/tests/files/ptrmap_13.gno @@ -0,0 +1,22 @@ +// PKGPATH: gno.land/r/ptr_map +package ptr_map + +type MyStruct struct { + Index int +} + +var m = make(map[*int]string) +var a, b = 1, 2 +var s = []*int{&a, &b} +var myStruct = MyStruct{Index: 0} + +func init() { + m[s[myStruct.Index]] = "a" +} + +func main() { + println(m[s[myStruct.Index]]) +} + +// Output: +// a diff --git a/gnovm/tests/files/ptrmap_14.gno b/gnovm/tests/files/ptrmap_14.gno new file mode 100644 index 00000000000..5eb4c436def --- /dev/null +++ b/gnovm/tests/files/ptrmap_14.gno @@ -0,0 +1,20 @@ +// PKGPATH: gno.land/r/ptr_map +package ptr_map + +var ( + m = map[*int]string{} + a = 0 + ptr *int = &a // A pointer to an int + i1 **int = &ptr +) + +func init() { + m[*i1] = "first key" +} + +func main() { + println(m[*i1]) // Output: first key +} + +// Output: +// first key diff --git a/gnovm/tests/files/ptrmap_15.gno b/gnovm/tests/files/ptrmap_15.gno new file mode 100644 index 00000000000..b900fc4741f --- /dev/null +++ b/gnovm/tests/files/ptrmap_15.gno @@ -0,0 +1,23 @@ +// PKGPATH: gno.land/r/ptr_map +package ptr_map + +type MyStruct struct { + Key *int +} + +var ( + m = map[*int]string{} + i1 = MyStruct{Key: new(int)} +) + +func init() { + *i1.Key = 1 // Set the value of the pointer + m[i1.Key] = "first key" +} + +func main() { + println(m[i1.Key]) // Output: first key +} + +// Output: +// first key diff --git a/gnovm/tests/files/ptrmap_16.gno b/gnovm/tests/files/ptrmap_16.gno new file mode 100644 index 00000000000..40682a2c70e --- /dev/null +++ b/gnovm/tests/files/ptrmap_16.gno @@ -0,0 +1,24 @@ +// PKGPATH: gno.land/r/ptr_map +package ptr_map + +type Foo struct { + name string +} + +var ( + arr = [3]Foo{Foo{"a"}, Foo{"b"}, Foo{"c"}} + m = map[*Foo]string{} +) + +func init() { + m[&arr[0]] = "first key" +} + +func main() { + println(m[&arr[0]]) + println(m[&arr[1]] == "") +} + +// Output: +// first key +// true diff --git a/gnovm/tests/files/ptrmap_17.gno b/gnovm/tests/files/ptrmap_17.gno new file mode 100644 index 00000000000..4838ae988be --- /dev/null +++ b/gnovm/tests/files/ptrmap_17.gno @@ -0,0 +1,23 @@ +// PKGPATH: gno.land/r/ptr_map +package ptr_map + +type Foo struct { + name string +} + +var ( + arr = [3]Foo{Foo{"a"}, Foo{"b"}, Foo{"c"}} + m = map[*Foo]string{} + index = 0 +) + +func init() { + m[&arr[index]] = "first key" +} + +func main() { + println(m[&arr[index]]) +} + +// Output: +// first key diff --git a/gnovm/tests/files/ptrmap_18.gno b/gnovm/tests/files/ptrmap_18.gno new file mode 100644 index 00000000000..03c2d431d91 --- /dev/null +++ b/gnovm/tests/files/ptrmap_18.gno @@ -0,0 +1,33 @@ +// PKGPATH: gno.land/r/ptr_map +package ptr_map + +var ( + m = map[*int]int{} + i = 0 +) + +func AddToMap(value int) { + m[&i] = value +} + +func GetFromMap() int { + return m[&i] +} + +func init() { + i = 1 + AddToMap(5) +} + +func main() { + r := GetFromMap() + println(r == 5) + + i = 2 + r = GetFromMap() + println(r == 5) +} + +// Output: +// true +// true diff --git a/gnovm/tests/files/ptrmap_19.gno b/gnovm/tests/files/ptrmap_19.gno new file mode 100644 index 00000000000..2a3e6b5240f --- /dev/null +++ b/gnovm/tests/files/ptrmap_19.gno @@ -0,0 +1,37 @@ +// PKGPATH: gno.land/r/ptr_map +package ptr_map + +var ( + m = map[*int]int{} + i = 0 +) + +func AddToMap(value int) { + m[&i] = value +} + +func GetFromMap() int { + i := 0 + { + { + return m[&i] + } + } +} + +func init() { + AddToMap(5) +} + +func main() { + r := GetFromMap() + println(r == 5) + + i = 2 + r = GetFromMap() + println(r == 5) +} + +// Output: +// false +// false diff --git a/gnovm/tests/files/ptrmap_2.gno b/gnovm/tests/files/ptrmap_2.gno new file mode 100644 index 00000000000..006de1274b7 --- /dev/null +++ b/gnovm/tests/files/ptrmap_2.gno @@ -0,0 +1,35 @@ +// PKGPATH: gno.land/r/ptr_map +package ptr_map + +var ( + m = map[*int]int{} + i = new(int) +) + +func AddToMap(value int) { + m[i] = value +} + +func GetFromMap() int { + return m[i] +} + +func init() { + *i = 1 + AddToMap(5) +} + +// ----above is initialized and persisted before main is executed. + +func main() { + r := GetFromMap() + println(r == 5) + + *i = 2 // this changes TV, also Base of a pointer value + r = GetFromMap() + println(r == 5) +} + +// Output: +// true +// true diff --git a/gnovm/tests/files/ptrmap_20.gno b/gnovm/tests/files/ptrmap_20.gno new file mode 100644 index 00000000000..5efe9afaac0 --- /dev/null +++ b/gnovm/tests/files/ptrmap_20.gno @@ -0,0 +1,38 @@ +// PKGPATH: gno.land/r/ptr_map +package ptr_map + +var ( + m = map[**int]int{} + i = new(int) +) + +func AddToMap(value int) { + m[&i] = value +} + +func GetFromMap() int { + return m[&i] +} + +func init() { + *i = 1 + AddToMap(5) +} + +func main() { + r := GetFromMap() + println(r == 5) + + var j = 0 + j1 := &j + println(m[&j1]) + + *i = 2 + r = GetFromMap() + println(r == 5) +} + +// Output: +// true +// 0 +// true \ No newline at end of file diff --git a/gnovm/tests/files/ptrmap_22.gno b/gnovm/tests/files/ptrmap_22.gno new file mode 100644 index 00000000000..653a6f2ffe8 --- /dev/null +++ b/gnovm/tests/files/ptrmap_22.gno @@ -0,0 +1,30 @@ +// PKGPATH: gno.land/r/ptr_map +package ptr_map + +type MyStruct struct { + Name string + Age int +} + +var ( + m = map[*MyStruct]string{} + i1 = &MyStruct{Name: "alice", Age: 2} +) + +func init() { + m[i1] = "first key" + println(m[i1]) +} + +func main() { + i2 := *i1 + println(m[&i2] == "") + + i1.Age = 3 + println(m[i1]) +} + +// Output: +// first key +// true +// first key diff --git a/gnovm/tests/files/ptrmap_23.gno b/gnovm/tests/files/ptrmap_23.gno new file mode 100644 index 00000000000..a587d924bc7 --- /dev/null +++ b/gnovm/tests/files/ptrmap_23.gno @@ -0,0 +1,17 @@ +// PKGPATH: gno.land/r/ptr_map +package ptr_map + +var arr = [2]int{1, 2} +var m = map[*[2]int]string{} + +func init() { + m[&arr] = "ok" +} + +func main() { + + println(m[&arr]) // Output: example +} + +// Output: +// ok diff --git a/gnovm/tests/files/ptrmap_24.gno b/gnovm/tests/files/ptrmap_24.gno new file mode 100644 index 00000000000..2c0890f47d0 --- /dev/null +++ b/gnovm/tests/files/ptrmap_24.gno @@ -0,0 +1,28 @@ +// PKGPATH: gno.land/r/ptr_map +package ptr_map + +type Foo struct { + name string +} + +type MyStruct struct { + Name string + Age int + key Foo +} + +var ( + m = map[*Foo]string{} + i1 = MyStruct{Name: "alice", Age: 2, key: Foo{name: "bob"}} +) + +func init() { + m[&i1.key] = "first key" +} + +func main() { + println(m[&i1.key]) +} + +// Output: +// first key diff --git a/gnovm/tests/files/ptrmap_25.gno b/gnovm/tests/files/ptrmap_25.gno new file mode 100644 index 00000000000..ebfa1940ce9 --- /dev/null +++ b/gnovm/tests/files/ptrmap_25.gno @@ -0,0 +1,34 @@ +// PKGPATH: gno.land/r/ptr_map +package ptr_map + +var ( + m = map[*int]int{} + a, b, c = 1, 2, 3 + s = []*int{&a, &b, &c} +) + +func AddToMap(value int) { + m[&*s[0]] = value +} + +func GetFromMap() int { + return m[&*s[0]] +} + +func init() { + AddToMap(5) +} + +func main() { + r := GetFromMap() + println(r == 5) + + a = 0 + + r = GetFromMap() + println(r == 5) +} + +// Output: +// true +// true diff --git a/gnovm/tests/files/ptrmap_26.gno b/gnovm/tests/files/ptrmap_26.gno new file mode 100644 index 00000000000..8b8faf97a2b --- /dev/null +++ b/gnovm/tests/files/ptrmap_26.gno @@ -0,0 +1,33 @@ +// PKGPATH: gno.land/r/ptr_map +package ptr_map + +var ( + m = map[*int]int{} + i = new(int) +) + +func AddToMap(value int) { + m[&*i] = value +} + +func GetFromMap() int { + return m[&*i] +} + +func init() { + *i = 1 + AddToMap(5) +} + +func main() { + r := GetFromMap() + println(r == 5) + + *i = 2 + r = GetFromMap() + println(r == 5) +} + +// Output: +// true +// true diff --git a/gnovm/tests/files/ptrmap_3.gno b/gnovm/tests/files/ptrmap_3.gno new file mode 100644 index 00000000000..d89c696e2f0 --- /dev/null +++ b/gnovm/tests/files/ptrmap_3.gno @@ -0,0 +1,34 @@ +// PKGPATH: gno.land/r/ptr_map +package ptr_map + +var ( + m = map[*int]int{} + i = new(int) +) + +func AddToMap(value int) { + m[i] = value +} + +func GetFromMap() int { + j := *i + return m[&j] +} + +func init() { + *i = 1 + AddToMap(5) +} + +func main() { + r := GetFromMap() + println(r == 5) + + *i = 2 + r = GetFromMap() + println(r == 5) +} + +// Output: +// false +// false diff --git a/gnovm/tests/files/ptrmap_4.gno b/gnovm/tests/files/ptrmap_4.gno new file mode 100644 index 00000000000..e8f6f2bfdb1 --- /dev/null +++ b/gnovm/tests/files/ptrmap_4.gno @@ -0,0 +1,30 @@ +// PKGPATH: gno.land/r/ptr_map +package ptr_map + +type S struct { + i int +} + +var m = make(map[*S]string) // Initialize the map +var sArr = make([]*S, 0, 4) // Use a slice of pointers + +func init() { + // Append pointers to the slice + sArr = append(sArr, &S{1}, &S{2}, &S{3}) + m[sArr[1]] = "a" +} + +func main() { + // Create a new slice without reallocating memory for existing elements + newArr := append(sArr[:1], sArr[2:]...) // same base array + + // Compare pointers directly + println(m[sArr[1]] == m[newArr[1]]) + println(m[sArr[1]] == "") + println(m[newArr[1]] == "") +} + +// Output: +// true +// true +// true diff --git a/gnovm/tests/files/ptrmap_5.gno b/gnovm/tests/files/ptrmap_5.gno new file mode 100644 index 00000000000..a38817867e5 --- /dev/null +++ b/gnovm/tests/files/ptrmap_5.gno @@ -0,0 +1,28 @@ +// PKGPATH: gno.land/r/ptr_map +package ptr_map + +type S struct { + i int +} + +var m = make(map[*S]string) // Initialize the map +var sArr = []*S{&S{1}, &S{2}, &S{3}} // Use a slice of pointers + +func init() { + m[sArr[1]] = "a" +} + +func main() { + // Create a new slice without reallocating memory for existing elements + newArr := append(sArr[:1], sArr[2:]...) // same base array + + // Compare pointers directly + println(sArr[1] == newArr[1]) + println(m[sArr[1]] == m[newArr[1]]) + println(m[newArr[1]] == "") +} + +// Output: +// true +// true +// true diff --git a/gnovm/tests/files/ptrmap_6.gno b/gnovm/tests/files/ptrmap_6.gno new file mode 100644 index 00000000000..1af7442e09f --- /dev/null +++ b/gnovm/tests/files/ptrmap_6.gno @@ -0,0 +1,37 @@ +// PKGPATH: gno.land/r/ptr_map +package ptr_map + +type S struct { + i int +} + +var m = make(map[*S]string) // Initialize the map +var sArr = make([]*S, 0, 4) // Use a slice of pointers + +func init() { + a := S{1} + // Append pointers to the slice + sArr = append(sArr, &a, &S{2}, &S{3}) + println(&a == sArr[0]) + m[sArr[1]] = "a" +} + +func main() { + // Create a new slice without reallocating memory for existing elements + newArr := append(sArr[:1], sArr[2:]...) + + newArr = append(newArr, &S{4}) + newArr = append(newArr, &S{5}) + newArr = append(newArr, &S{6}) // reallocate array + + // Compare pointers directly + println(sArr[1] == newArr[1]) + println(m[sArr[1]] == m[newArr[1]]) + println(m[newArr[1]] == "") +} + +// Output: +// true +// true +// true +// true diff --git a/gnovm/tests/files/ptrmap_7.gno b/gnovm/tests/files/ptrmap_7.gno new file mode 100644 index 00000000000..443a112c6d8 --- /dev/null +++ b/gnovm/tests/files/ptrmap_7.gno @@ -0,0 +1,41 @@ +// PKGPATH: gno.land/r/ptr_map +package ptr_map + +import "fmt" + +type S struct { + i int +} + +var m = make(map[*byte]string) // Initialize the map +var sArr = make([]*byte, 0, 4) // Use a slice of pointers +var a, b, c = byte('a'), byte('b'), byte('c') +var d, e, f = byte('d'), byte('e'), byte('f') + +func init() { + // Append pointers to the slice + sArr = append(sArr, &a, &b, &c) + m[sArr[1]] = "ok" + println(&b == sArr[1]) +} + +func main() { + // Create a new slice without reallocating memory for existing elements + newArr := append(sArr[:1], sArr[2:]...) + + newArr = append(newArr, &d) + newArr = append(newArr, &e) + // a, c, d, e, f + newArr = append(newArr, &f) // reallocation + + // Compare pointers directly + println(sArr[1] == newArr[1]) + println(m[sArr[1]] == m[newArr[1]]) + println(m[newArr[1]] == "") // underlying base array changed +} + +// Output: +// true +// true +// true +// true diff --git a/gnovm/tests/files/ptrmap_8.gno b/gnovm/tests/files/ptrmap_8.gno new file mode 100644 index 00000000000..0985644e9d9 --- /dev/null +++ b/gnovm/tests/files/ptrmap_8.gno @@ -0,0 +1,26 @@ +// PKGPATH: gno.land/r/ptr_map +package ptr_map + +var m = make(map[*int]string) +var m2 = make(map[int]int) + +var a, b, c = 1, 2, 3 +var s = []*int{&a, &b, &c} +var s2 []*int + +func init() { + m2[0] = 0 + m[s[m2[0]]] = "a" + s2 = append(s[:1], s[1:]...) +} + +func main() { + println(m[s[m2[0]]]) + println(s[m2[0]] == s2[m2[0]]) + println(m[s[m2[0]]] == m[s2[m2[0]]]) +} + +// Output: +// a +// true +// true diff --git a/gnovm/tests/files/ptrmap_9.gno b/gnovm/tests/files/ptrmap_9.gno new file mode 100644 index 00000000000..2756ad9acc7 --- /dev/null +++ b/gnovm/tests/files/ptrmap_9.gno @@ -0,0 +1,27 @@ +// PKGPATH: gno.land/r/ptr_map +package ptr_map + +var m = make(map[*int]string) +var m2 = make(map[int]int) + +var s []*int +var s2 []*int + +func init() { + s = append(s, new(int)) + m2[0] = 0 + // s[m2[0]] is pointer value, + m[s[m2[0]]] = "a" + s2 = append(s[:1], s[1:]...) +} + +func main() { + println(m[s[m2[0]]]) + println(s[m2[0]] == s2[m2[0]]) + println(m[s[m2[0]]] == m[s2[m2[0]]]) +} + +// Output: +// a +// true +// true From 238d5d8d09e2dfdd19f983fffef020b09b3be5fc Mon Sep 17 00:00:00 2001 From: Lee ByeongJun Date: Mon, 20 Jan 2025 23:24:02 +0900 Subject: [PATCH 08/12] fix(avl): partial revert from #1881 (#3534) # Description closes #3533 Changed only the `node.gno` files to before #1881, but kept the implementation of the `TraverseInRange` function that was modified in #3393. --- examples/gno.land/p/demo/avl/node.gno | 223 ++++++++++----------- examples/gno.land/p/demo/avl/node_test.gno | 178 +++++++++++++++- 2 files changed, 279 insertions(+), 122 deletions(-) diff --git a/examples/gno.land/p/demo/avl/node.gno b/examples/gno.land/p/demo/avl/node.gno index 7d4ddffff02..0ad8b0cecbf 100644 --- a/examples/gno.land/p/demo/avl/node.gno +++ b/examples/gno.land/p/demo/avl/node.gno @@ -46,7 +46,6 @@ func (node *Node) Value() interface{} { return node.value } -// _copy creates a copy of the node (excluding value). func (node *Node) _copy() *Node { if node.height == 0 { panic("Why are you copying a value node?") @@ -70,11 +69,13 @@ func (node *Node) Has(key string) (has bool) { } if node.height == 0 { return false + } else { + if key < node.key { + return node.getLeftNode().Has(key) + } else { + return node.getRightNode().Has(key) + } } - if key < node.key { - return node.getLeftNode().Has(key) - } - return node.getRightNode().Has(key) } // Get searches for a node with the given key in the subtree rooted at the node @@ -87,21 +88,21 @@ func (node *Node) Get(key string) (index int, value interface{}, exists bool) { if node.height == 0 { if node.key == key { return 0, node.value, true - } - if node.key < key { + } else if node.key < key { return 1, nil, false + } else { + return 0, nil, false + } + } else { + if key < node.key { + return node.getLeftNode().Get(key) + } else { + rightNode := node.getRightNode() + index, value, exists = rightNode.Get(key) + index += node.size - rightNode.size + return index, value, exists } - return 0, nil, false - } - - if key < node.key { - return node.getLeftNode().Get(key) } - - rightNode := node.getRightNode() - index, value, exists = rightNode.Get(key) - index += node.size - rightNode.size - return index, value, exists } // GetByIndex retrieves the key-value pair of the node at the given index @@ -110,15 +111,18 @@ func (node *Node) GetByIndex(index int) (key string, value interface{}) { if node.height == 0 { if index == 0 { return node.key, node.value + } else { + panic("GetByIndex asked for invalid index") + } + } else { + // TODO: could improve this by storing the sizes + leftNode := node.getLeftNode() + if index < leftNode.size { + return leftNode.GetByIndex(index) + } else { + return node.getRightNode().GetByIndex(index - leftNode.size) } - panic("GetByIndex asked for invalid index") - } - // TODO: could improve this by storing the sizes - leftNode := node.getLeftNode() - if index < leftNode.size { - return leftNode.GetByIndex(index) } - return node.getRightNode().GetByIndex(index - leftNode.size) } // Set inserts a new node with the given key-value pair into the subtree rooted at the node, @@ -129,50 +133,40 @@ func (node *Node) Set(key string, value interface{}) (newSelf *Node, updated boo if node == nil { return NewNode(key, value), false } - if node.height == 0 { - return node.setLeaf(key, value) - } - - node = node._copy() - if key < node.key { - node.leftNode, updated = node.getLeftNode().Set(key, value) + if key < node.key { + return &Node{ + key: node.key, + height: 1, + size: 2, + leftNode: NewNode(key, value), + rightNode: node, + }, false + } else if key == node.key { + return NewNode(key, value), true + } else { + return &Node{ + key: key, + height: 1, + size: 2, + leftNode: node, + rightNode: NewNode(key, value), + }, false + } } else { - node.rightNode, updated = node.getRightNode().Set(key, value) - } - - if updated { - return node, updated - } - - node.calcHeightAndSize() - return node.balance(), updated -} - -// setLeaf inserts a new leaf node with the given key-value pair into the subtree rooted at the node, -// and returns the new root of the subtree and whether an existing node was updated. -func (node *Node) setLeaf(key string, value interface{}) (newSelf *Node, updated bool) { - if key == node.key { - return NewNode(key, value), true - } - - if key < node.key { - return &Node{ - key: node.key, - height: 1, - size: 2, - leftNode: NewNode(key, value), - rightNode: node, - }, false + node = node._copy() + if key < node.key { + node.leftNode, updated = node.getLeftNode().Set(key, value) + } else { + node.rightNode, updated = node.getRightNode().Set(key, value) + } + if updated { + return node, updated + } else { + node.calcHeightAndSize() + return node.balance(), updated + } } - - return &Node{ - key: key, - height: 1, - size: 2, - leftNode: node, - rightNode: NewNode(key, value), - }, false } // Remove deletes the node with the given key from the subtree rooted at the node. @@ -187,49 +181,47 @@ func (node *Node) Remove(key string) ( if node.height == 0 { if key == node.key { return nil, "", node.value, true + } else { + return node, "", nil, false } - return node, "", nil, false - } - if key < node.key { - var newLeftNode *Node - newLeftNode, newKey, value, removed = node.getLeftNode().Remove(key) - if !removed { - return node, "", value, false - } - if newLeftNode == nil { // left node held value, was removed - return node.rightNode, node.key, value, true + } else { + if key < node.key { + var newLeftNode *Node + newLeftNode, newKey, value, removed = node.getLeftNode().Remove(key) + if !removed { + return node, "", value, false + } else if newLeftNode == nil { // left node held value, was removed + return node.rightNode, node.key, value, true + } + node = node._copy() + node.leftNode = newLeftNode + node.calcHeightAndSize() + node = node.balance() + return node, newKey, value, true + } else { + var newRightNode *Node + newRightNode, newKey, value, removed = node.getRightNode().Remove(key) + if !removed { + return node, "", value, false + } else if newRightNode == nil { // right node held value, was removed + return node.leftNode, "", value, true + } + node = node._copy() + node.rightNode = newRightNode + if newKey != "" { + node.key = newKey + } + node.calcHeightAndSize() + node = node.balance() + return node, "", value, true } - node = node._copy() - node.leftNode = newLeftNode - node.calcHeightAndSize() - node = node.balance() - return node, newKey, value, true - } - - var newRightNode *Node - newRightNode, newKey, value, removed = node.getRightNode().Remove(key) - if !removed { - return node, "", value, false } - if newRightNode == nil { // right node held value, was removed - return node.leftNode, "", value, true - } - node = node._copy() - node.rightNode = newRightNode - if newKey != "" { - node.key = newKey - } - node.calcHeightAndSize() - node = node.balance() - return node, "", value, true } -// getLeftNode returns the left child of the node. func (node *Node) getLeftNode() *Node { return node.leftNode } -// getRightNode returns the right child of the node. func (node *Node) getRightNode() *Node { return node.rightNode } @@ -287,29 +279,30 @@ func (node *Node) calcBalance() int { // TODO: optimize balance & rotate func (node *Node) balance() (newSelf *Node) { balance := node.calcBalance() - if balance >= -1 { - return node - } if balance > 1 { if node.getLeftNode().calcBalance() >= 0 { // Left Left Case return node.rotateRight() + } else { + // Left Right Case + left := node.getLeftNode() + node.leftNode = left.rotateLeft() + return node.rotateRight() } - // Left Right Case - left := node.getLeftNode() - node.leftNode = left.rotateLeft() - return node.rotateRight() } - - if node.getRightNode().calcBalance() <= 0 { - // Right Right Case - return node.rotateLeft() + if balance < -1 { + if node.getRightNode().calcBalance() <= 0 { + // Right Right Case + return node.rotateLeft() + } else { + // Right Left Case + right := node.getRightNode() + node.rightNode = right.rotateRight() + return node.rotateLeft() + } } - - // Right Left Case - right := node.getRightNode() - node.rightNode = right.rotateRight() - return node.rotateLeft() + // Nothing changed + return node } // Shortcut for TraverseInRange. diff --git a/examples/gno.land/p/demo/avl/node_test.gno b/examples/gno.land/p/demo/avl/node_test.gno index 3682cbc7c80..10c742d8cc6 100644 --- a/examples/gno.land/p/demo/avl/node_test.gno +++ b/examples/gno.land/p/demo/avl/node_test.gno @@ -4,6 +4,8 @@ import ( "sort" "strings" "testing" + + "gno.land/p/demo/ufmt" ) func TestTraverseByOffset(t *testing.T) { @@ -550,6 +552,175 @@ func TestRotateAndBalance(t *testing.T) { } } +func TestRemoveFromEmptyTree(t *testing.T) { + var tree *Node + newTree, _, val, removed := tree.Remove("NonExistent") + if newTree != nil { + t.Errorf("Removing from an empty tree should still be nil tree.") + } + if val != nil || removed { + t.Errorf("Expected no value and removed=false when removing from empty tree.") + } +} + +func TestBalanceAfterRemoval(t *testing.T) { + tests := []struct { + name string + insertKeys []string + removeKey string + expectedBalance int + }{ + { + name: "balance after removing right node", + insertKeys: []string{"B", "A", "D", "C", "E"}, + removeKey: "E", + expectedBalance: 0, + }, + { + name: "balance after removing left node", + insertKeys: []string{"D", "B", "E", "A", "C"}, + removeKey: "A", + expectedBalance: 0, + }, + { + name: "ensure no lean after removal", + insertKeys: []string{"C", "B", "E", "A", "D", "F"}, + removeKey: "F", + expectedBalance: -1, + }, + { + name: "descending order insert, remove middle node", + insertKeys: []string{"E", "D", "C", "B", "A"}, + removeKey: "C", + expectedBalance: 0, + }, + { + name: "ascending order insert, remove middle node", + insertKeys: []string{"A", "B", "C", "D", "E"}, + removeKey: "C", + expectedBalance: 0, + }, + { + name: "duplicate key insert, remove the duplicated key", + insertKeys: []string{"C", "B", "C", "A", "D"}, + removeKey: "C", + expectedBalance: 1, + }, + { + name: "complex rotation case", + insertKeys: []string{"H", "B", "A", "C", "E", "D", "F", "G"}, + removeKey: "B", + expectedBalance: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var tree *Node + for _, key := range tt.insertKeys { + tree, _ = tree.Set(key, nil) + } + + tree, _, _, _ = tree.Remove(tt.removeKey) + + balance := tree.calcBalance() + if balance != tt.expectedBalance { + t.Errorf("Expected balance factor %d, got %d", tt.expectedBalance, balance) + } + + if balance < -1 || balance > 1 { + t.Errorf("Tree is unbalanced with factor %d", balance) + } + + if errMsg := checkSubtreeBalance(t, tree); errMsg != "" { + t.Errorf("AVL property violation after removal: %s", errMsg) + } + }) + } +} + +func TestBSTProperty(t *testing.T) { + var tree *Node + keys := []string{"D", "B", "F", "A", "C", "E", "G"} + for _, key := range keys { + tree, _ = tree.Set(key, nil) + } + + var result []string + inorderTraversal(t, tree, &result) + + for i := 1; i < len(result); i++ { + if result[i] < result[i-1] { + t.Errorf("BST property violated: %s < %s (index %d)", + result[i], result[i-1], i) + } + } +} + +// inorderTraversal performs an inorder traversal of the tree and returns the keys in a list. +func inorderTraversal(t *testing.T, node *Node, result *[]string) { + t.Helper() + + if node == nil { + return + } + // leaf + if node.height == 0 { + *result = append(*result, node.key) + return + } + inorderTraversal(t, node.leftNode, result) + inorderTraversal(t, node.rightNode, result) +} + +// checkSubtreeBalance checks if all nodes under the given node satisfy the AVL tree conditions. +// The balance factor of all nodes must be ∈ [-1, +1] +func checkSubtreeBalance(t *testing.T, node *Node) string { + t.Helper() + + if node == nil { + return "" + } + + if node.IsLeaf() { + // leaf node must be height=0, size=1 + if node.height != 0 { + return ufmt.Sprintf("Leaf node %s has height %d, expected 0", node.Key(), node.height) + } + if node.size != 1 { + return ufmt.Sprintf("Leaf node %s has size %d, expected 1", node.Key(), node.size) + } + return "" + } + + // check balance factor for current node + balanceFactor := node.calcBalance() + if balanceFactor < -1 || balanceFactor > 1 { + return ufmt.Sprintf("Node %s is unbalanced: balanceFactor=%d", node.Key(), balanceFactor) + } + + // check height / size relationship for children + left, right := node.getLeftNode(), node.getRightNode() + expectedHeight := maxInt8(left.height, right.height) + 1 + if node.height != expectedHeight { + return ufmt.Sprintf("Node %s has incorrect height %d, expected %d", node.Key(), node.height, expectedHeight) + } + expectedSize := left.Size() + right.Size() + if node.size != expectedSize { + return ufmt.Sprintf("Node %s has incorrect size %d, expected %d", node.Key(), node.size, expectedSize) + } + + // recursively check the left/right subtree + if errMsg := checkSubtreeBalance(t, left); errMsg != "" { + return errMsg + } + if errMsg := checkSubtreeBalance(t, right); errMsg != "" { + return errMsg + } + + return "" +} + func slicesEqual(w1, w2 []string) bool { if len(w1) != len(w2) { return false @@ -562,13 +733,6 @@ func slicesEqual(w1, w2 []string) bool { return true } -func maxint8(a, b int8) int8 { - if a > b { - return a - } - return b -} - func reverseSlice(ss []string) { for i := 0; i < len(ss)/2; i++ { j := len(ss) - 1 - i From fc40183ada9d970d25b8bbf0017fcecb6ad82458 Mon Sep 17 00:00:00 2001 From: Petar Dambovaliev Date: Mon, 20 Jan 2025 16:56:02 +0100 Subject: [PATCH 09/12] fix: addressability (#3198) Fixes https://github.com/gnolang/gno/issues/2299 and https://github.com/gnolang/gno/issues/2840 --------- Co-authored-by: deelawn Co-authored-by: ltzmaxwell --- gnovm/pkg/gnolang/go2gno.go | 2 + gnovm/pkg/gnolang/nodes.go | 241 ++++++++++++++---- gnovm/pkg/gnolang/op_types.go | 5 +- gnovm/pkg/gnolang/preprocess.go | 86 ++++++- gnovm/pkg/gnolang/types.go | 2 + gnovm/stdlibs/crypto/sha256/sha256_test.gno | 3 +- gnovm/tests/files/addressable_1.gno | 38 +++ gnovm/tests/files/addressable_10.gno | 12 + gnovm/tests/files/addressable_10a_err.gno | 14 + gnovm/tests/files/addressable_10b_err.gno | 12 + gnovm/tests/files/addressable_11.gno | 39 +++ gnovm/tests/files/addressable_1a_err.gno | 8 + gnovm/tests/files/addressable_1b_err.gno | 8 + gnovm/tests/files/addressable_1c_err.gno | 13 + gnovm/tests/files/addressable_1d_err.gno | 12 + gnovm/tests/files/addressable_2.gno | 40 +++ gnovm/tests/files/addressable_2a_err.gno | 8 + gnovm/tests/files/addressable_2b_err.gno | 12 + gnovm/tests/files/addressable_3.gno | 105 ++++++++ gnovm/tests/files/addressable_3a_err.gno | 14 + gnovm/tests/files/addressable_3b_err.gno | 16 ++ gnovm/tests/files/addressable_3c_err.gno | 17 ++ gnovm/tests/files/addressable_3d_err.gno | 8 + gnovm/tests/files/addressable_4.gno | 23 ++ gnovm/tests/files/addressable_4a_err.gno | 9 + gnovm/tests/files/addressable_5.gno | 11 + .../files/addressable_5a_err_stdlibs.gno | 11 + .../files/addressable_5b_err_stdlibs.gno | 11 + gnovm/tests/files/addressable_5c_err.gno | 10 + gnovm/tests/files/addressable_5d_err.gno | 10 + gnovm/tests/files/addressable_6.gno | 27 ++ gnovm/tests/files/addressable_6a_err.gno | 10 + gnovm/tests/files/addressable_6b_err.gno | 14 + gnovm/tests/files/addressable_6c_err.gno | 10 + gnovm/tests/files/addressable_6d_err.gno | 12 + gnovm/tests/files/addressable_7a_err.gno | 12 + gnovm/tests/files/addressable_7b_err.gno | 8 + gnovm/tests/files/addressable_8.gno | 9 + gnovm/tests/files/addressable_8a_err.gno | 8 + gnovm/tests/files/addressable_9.gno | 32 +++ gnovm/tests/files/addressable_9a_err.gno | 10 + gnovm/tests/files/addressable_9b_err.gno | 15 ++ 42 files changed, 907 insertions(+), 60 deletions(-) create mode 100644 gnovm/tests/files/addressable_1.gno create mode 100644 gnovm/tests/files/addressable_10.gno create mode 100644 gnovm/tests/files/addressable_10a_err.gno create mode 100644 gnovm/tests/files/addressable_10b_err.gno create mode 100644 gnovm/tests/files/addressable_11.gno create mode 100644 gnovm/tests/files/addressable_1a_err.gno create mode 100644 gnovm/tests/files/addressable_1b_err.gno create mode 100644 gnovm/tests/files/addressable_1c_err.gno create mode 100644 gnovm/tests/files/addressable_1d_err.gno create mode 100644 gnovm/tests/files/addressable_2.gno create mode 100644 gnovm/tests/files/addressable_2a_err.gno create mode 100644 gnovm/tests/files/addressable_2b_err.gno create mode 100644 gnovm/tests/files/addressable_3.gno create mode 100644 gnovm/tests/files/addressable_3a_err.gno create mode 100644 gnovm/tests/files/addressable_3b_err.gno create mode 100644 gnovm/tests/files/addressable_3c_err.gno create mode 100644 gnovm/tests/files/addressable_3d_err.gno create mode 100644 gnovm/tests/files/addressable_4.gno create mode 100644 gnovm/tests/files/addressable_4a_err.gno create mode 100644 gnovm/tests/files/addressable_5.gno create mode 100644 gnovm/tests/files/addressable_5a_err_stdlibs.gno create mode 100644 gnovm/tests/files/addressable_5b_err_stdlibs.gno create mode 100644 gnovm/tests/files/addressable_5c_err.gno create mode 100644 gnovm/tests/files/addressable_5d_err.gno create mode 100644 gnovm/tests/files/addressable_6.gno create mode 100644 gnovm/tests/files/addressable_6a_err.gno create mode 100644 gnovm/tests/files/addressable_6b_err.gno create mode 100644 gnovm/tests/files/addressable_6c_err.gno create mode 100644 gnovm/tests/files/addressable_6d_err.gno create mode 100644 gnovm/tests/files/addressable_7a_err.gno create mode 100644 gnovm/tests/files/addressable_7b_err.gno create mode 100644 gnovm/tests/files/addressable_8.gno create mode 100644 gnovm/tests/files/addressable_8a_err.gno create mode 100644 gnovm/tests/files/addressable_9.gno create mode 100644 gnovm/tests/files/addressable_9a_err.gno create mode 100644 gnovm/tests/files/addressable_9b_err.gno diff --git a/gnovm/pkg/gnolang/go2gno.go b/gnovm/pkg/gnolang/go2gno.go index 4c9de87a6a7..34686dc4cc1 100644 --- a/gnovm/pkg/gnolang/go2gno.go +++ b/gnovm/pkg/gnolang/go2gno.go @@ -225,6 +225,8 @@ func Go2Gno(fs *token.FileSet, gon ast.Node) (n Node) { } case *ast.FuncLit: type_ := Go2Gno(fs, gon.Type).(*FuncTypeExpr) + type_.IsClosure = true + return &FuncLitExpr{ Type: *type_, Body: toBody(fs, gon.Body), diff --git a/gnovm/pkg/gnolang/nodes.go b/gnovm/pkg/gnolang/nodes.go index 0f1f4388623..445968a2c9c 100644 --- a/gnovm/pkg/gnolang/nodes.go +++ b/gnovm/pkg/gnolang/nodes.go @@ -352,28 +352,11 @@ var ( type Expr interface { Node - assertExpr() + addressability() addressabilityStatus } type Exprs []Expr -// non-pointer receiver to help make immutable. -func (*NameExpr) assertExpr() {} -func (*BasicLitExpr) assertExpr() {} -func (*BinaryExpr) assertExpr() {} -func (*CallExpr) assertExpr() {} -func (*IndexExpr) assertExpr() {} -func (*SelectorExpr) assertExpr() {} -func (*SliceExpr) assertExpr() {} -func (*StarExpr) assertExpr() {} -func (*RefExpr) assertExpr() {} -func (*TypeAssertExpr) assertExpr() {} -func (*UnaryExpr) assertExpr() {} -func (*CompositeLitExpr) assertExpr() {} -func (*KeyValueExpr) assertExpr() {} -func (*FuncLitExpr) assertExpr() {} -func (*ConstExpr) assertExpr() {} - var ( _ Expr = &NameExpr{} _ Expr = &BasicLitExpr{} @@ -410,6 +393,10 @@ type NameExpr struct { Type NameExprType } +func (x *NameExpr) addressability() addressabilityStatus { + return addressabilityStatusSatisfied +} + type NameExprs []NameExpr type BasicLitExpr struct { @@ -421,6 +408,10 @@ type BasicLitExpr struct { Value string } +func (x *BasicLitExpr) addressability() addressabilityStatus { + return addressabilityStatusUnsatisfied +} + type BinaryExpr struct { // (Left Op Right) Attributes Left Expr // left operand @@ -428,26 +419,54 @@ type BinaryExpr struct { // (Left Op Right) Right Expr // right operand } +func (x *BinaryExpr) addressability() addressabilityStatus { + return addressabilityStatusUnsatisfied +} + type CallExpr struct { // Func(Args) Attributes - Func Expr // function expression - Args Exprs // function arguments, if any. - Varg bool // if true, final arg is variadic. - NumArgs int // len(Args) or len(Args[0].Results) + Func Expr // function expression + Args Exprs // function arguments, if any. + Varg bool // if true, final arg is variadic. + NumArgs int // len(Args) or len(Args[0].Results) + Addressability addressabilityStatus +} + +func (x *CallExpr) addressability() addressabilityStatus { + return x.Addressability } type IndexExpr struct { // X[Index] Attributes - X Expr // expression - Index Expr // index expression - HasOK bool // if true, is form: `value, ok := []` + X Expr // expression + Index Expr // index expression + HasOK bool // if true, is form: `value, ok := [] + Addressability addressabilityStatus +} + +func (x *IndexExpr) addressability() addressabilityStatus { + // If not set in TRANS_LEAVE, defer to the the child expression's addressability. + if x.Addressability == addressabilityStatusNotApplicable { + return x.X.addressability() + } + + return x.Addressability } type SelectorExpr struct { // X.Sel Attributes - X Expr // expression - Path ValuePath // set by preprocessor. - Sel Name // field selector + X Expr // expression + Path ValuePath // set by preprocessor. + Sel Name // field selector + IsAddressable bool // true if X is a pointer +} + +func (x *SelectorExpr) addressability() addressabilityStatus { + if x.IsAddressable || x.X.addressability() == addressabilityStatusSatisfied { + return addressabilityStatusSatisfied + } + + return addressabilityStatusUnsatisfied } type SliceExpr struct { // X[Low:High:Max] @@ -458,6 +477,10 @@ type SliceExpr struct { // X[Low:High:Max] Max Expr // maximum capacity of slice; or nil; added in Go 1.2 } +func (x *SliceExpr) addressability() addressabilityStatus { + return addressabilityStatusUnsatisfied +} + // A StarExpr node represents an expression of the form // "*" Expression. Semantically it could be a unary "*" // expression, or a pointer type. @@ -466,16 +489,33 @@ type StarExpr struct { // *X X Expr // operand } +func (x *StarExpr) addressability() addressabilityStatus { + return addressabilityStatusSatisfied +} + type RefExpr struct { // &X Attributes X Expr // operand } +func (x *RefExpr) addressability() addressabilityStatus { + return x.X.addressability() +} + type TypeAssertExpr struct { // X.(Type) Attributes - X Expr // expression. - Type Expr // asserted type, never nil. - HasOK bool // if true, is form: `_, ok := .()`. + X Expr // expression. + Type Expr // asserted type, never nil. + HasOK bool // if true, is form: `_, ok := .()`. + IsAddressable bool +} + +func (x *TypeAssertExpr) addressability() addressabilityStatus { + if x.IsAddressable { + return addressabilityStatusSatisfied + } + + return addressabilityStatusUnsatisfied } // A UnaryExpr node represents a unary expression. Unary @@ -488,12 +528,25 @@ type UnaryExpr struct { // (Op X) Op Word // operator } +func (x *UnaryExpr) addressability() addressabilityStatus { + return x.X.addressability() +} + // MyType{:} struct, array, slice, and map // expressions. type CompositeLitExpr struct { Attributes - Type Expr // literal type; or nil - Elts KeyValueExprs // list of struct fields; if any + Type Expr // literal type; or nil + Elts KeyValueExprs // list of struct fields; if any + IsAddressable bool +} + +func (x *CompositeLitExpr) addressability() addressabilityStatus { + if x.IsAddressable { + return addressabilityStatusSatisfied + } + + return addressabilityStatusUnsatisfied } // Returns true if any elements are keyed. @@ -526,6 +579,10 @@ type KeyValueExpr struct { Value Expr // never nil } +func (x *KeyValueExpr) addressability() addressabilityStatus { + return addressabilityStatusNotApplicable +} + type KeyValueExprs []KeyValueExpr // A FuncLitExpr node represents a function literal. Here one @@ -539,6 +596,10 @@ type FuncLitExpr struct { HeapCaptures NameExprs // filled in findLoopUses1 } +func (x *FuncLitExpr) addressability() addressabilityStatus { + return addressabilityStatusUnsatisfied +} + // The preprocessor replaces const expressions // with *ConstExpr nodes. type ConstExpr struct { @@ -547,6 +608,10 @@ type ConstExpr struct { TypedValue } +func (x *ConstExpr) addressability() addressabilityStatus { + return addressabilityStatusUnsatisfied +} + // ---------------------------------------- // Type(Expressions) // @@ -565,6 +630,8 @@ type TypeExpr interface { assertTypeExpr() } +const typeExprAddressability = "the addressability method should not be called on Type Expressions" + // non-pointer receiver to help make immutable. func (x *FieldTypeExpr) assertTypeExpr() {} func (x *ArrayTypeExpr) assertTypeExpr() {} @@ -577,17 +644,6 @@ func (x *StructTypeExpr) assertTypeExpr() {} func (x *constTypeExpr) assertTypeExpr() {} func (x *MaybeNativeTypeExpr) assertTypeExpr() {} -func (x *FieldTypeExpr) assertExpr() {} -func (x *ArrayTypeExpr) assertExpr() {} -func (x *SliceTypeExpr) assertExpr() {} -func (x *InterfaceTypeExpr) assertExpr() {} -func (x *ChanTypeExpr) assertExpr() {} -func (x *FuncTypeExpr) assertExpr() {} -func (x *MapTypeExpr) assertExpr() {} -func (x *StructTypeExpr) assertExpr() {} -func (x *constTypeExpr) assertExpr() {} -func (x *MaybeNativeTypeExpr) assertExpr() {} - var ( _ TypeExpr = &FieldTypeExpr{} _ TypeExpr = &ArrayTypeExpr{} @@ -611,6 +667,10 @@ type FieldTypeExpr struct { Tag Expr } +func (x *FieldTypeExpr) addressability() addressabilityStatus { + panic(typeExprAddressability) +} + type FieldTypeExprs []FieldTypeExpr func (ftxz FieldTypeExprs) IsNamed() bool { @@ -639,18 +699,30 @@ type ArrayTypeExpr struct { Elt Expr // element type } +func (x *ArrayTypeExpr) addressability() addressabilityStatus { + panic(typeExprAddressability) +} + type SliceTypeExpr struct { Attributes Elt Expr // element type Vrd bool // variadic arg expression } +func (x *SliceTypeExpr) addressability() addressabilityStatus { + panic(typeExprAddressability) +} + type InterfaceTypeExpr struct { Attributes Methods FieldTypeExprs // list of methods Generic Name // for uverse generics } +func (x *InterfaceTypeExpr) addressability() addressabilityStatus { + panic(typeExprAddressability) +} + type ChanDir int const ( @@ -668,10 +740,19 @@ type ChanTypeExpr struct { Value Expr // value type } +func (x *ChanTypeExpr) addressability() addressabilityStatus { + panic(typeExprAddressability) +} + type FuncTypeExpr struct { Attributes - Params FieldTypeExprs // (incoming) parameters, if any. - Results FieldTypeExprs // (outgoing) results, if any. + Params FieldTypeExprs // (incoming) parameters, if any. + Results FieldTypeExprs // (outgoing) results, if any. + IsClosure bool +} + +func (x *FuncTypeExpr) addressability() addressabilityStatus { + panic(typeExprAddressability) } type MapTypeExpr struct { @@ -680,11 +761,19 @@ type MapTypeExpr struct { Value Expr // value type } +func (x *MapTypeExpr) addressability() addressabilityStatus { + panic(typeExprAddressability) +} + type StructTypeExpr struct { Attributes Fields FieldTypeExprs // list of field declarations } +func (x *StructTypeExpr) addressability() addressabilityStatus { + panic(typeExprAddressability) +} + // Like ConstExpr but for types. type constTypeExpr struct { Attributes @@ -692,12 +781,20 @@ type constTypeExpr struct { Type Type } +func (x *constTypeExpr) addressability() addressabilityStatus { + panic(typeExprAddressability) +} + // Only used for native func arguments type MaybeNativeTypeExpr struct { Attributes Type Expr } +func (x *MaybeNativeTypeExpr) addressability() addressabilityStatus { + panic(typeExprAddressability) +} + // ---------------------------------------- // Stmt // @@ -1531,6 +1628,7 @@ type BlockNode interface { GetPathForName(Store, Name) ValuePath GetBlockNodeForPath(Store, ValuePath) BlockNode GetIsConst(Store, Name) bool + GetIsConstAt(Store, ValuePath) bool GetLocalIndex(Name) (uint16, bool) GetValueRef(Store, Name, bool) *TypedValue GetStaticTypeOf(Store, Name) Type @@ -1548,12 +1646,13 @@ type BlockNode interface { // Embed in node to make it a BlockNode. type StaticBlock struct { Block - Types []Type - NumNames uint16 - Names []Name - Consts []Name // TODO consider merging with Names. - Externs []Name - Loc Location + Types []Type + NumNames uint16 + Names []Name + UnassignableNames []Name + Consts []Name // TODO consider merging with Names. + Externs []Name + Loc Location // temporary storage for rolling back redefinitions. oldValues []oldValue @@ -1738,6 +1837,10 @@ func (sb *StaticBlock) GetIsConst(store Store, n Name) bool { } } +func (sb *StaticBlock) GetIsConstAt(store Store, path ValuePath) bool { + return sb.GetBlockNodeForPath(store, path).GetStaticBlock().getLocalIsConst(path.Name) +} + // Returns true iff n is a local const defined name. func (sb *StaticBlock) getLocalIsConst(n Name) bool { for _, name := range sb.Consts { @@ -1748,6 +1851,32 @@ func (sb *StaticBlock) getLocalIsConst(n Name) bool { return false } +func (sb *StaticBlock) IsAssignable(store Store, n Name) bool { + _, ok := sb.GetLocalIndex(n) + bp := sb.GetParentNode(store) + un := sb.UnassignableNames + + for { + if ok { + for _, uname := range un { + if n == uname { + return false + } + } + + return true + } else if bp != nil { + _, ok = bp.GetLocalIndex(n) + un = bp.GetStaticBlock().UnassignableNames + bp = bp.GetParentNode(store) + } else if _, ok := UverseNode().GetLocalIndex(n); ok { + return false + } else { + return true + } + } +} + // Implements BlockNode. // XXX XXX what about uverse? func (sb *StaticBlock) GetStaticTypeOf(store Store, n Name) Type { @@ -2187,3 +2316,11 @@ func isHiddenResultVariable(name string) bool { } return false } + +type addressabilityStatus int + +const ( + addressabilityStatusNotApplicable addressabilityStatus = iota + addressabilityStatusSatisfied + addressabilityStatusUnsatisfied +) diff --git a/gnovm/pkg/gnolang/op_types.go b/gnovm/pkg/gnolang/op_types.go index 37b549fe14c..f7f3f75f91d 100644 --- a/gnovm/pkg/gnolang/op_types.go +++ b/gnovm/pkg/gnolang/op_types.go @@ -85,8 +85,9 @@ func (m *Machine) doOpFuncType() { } // Push func type. ft := &FuncType{ - Params: params, - Results: results, + Params: params, + Results: results, + IsClosure: x.IsClosure, } m.PushValue(TypedValue{ T: gTypeType, diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index 96e09642cd8..ca5834aa44e 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -234,6 +234,7 @@ func initStaticBlocks(store Store, ctx BlockNode, bn BlockNode) { nx := &n.NameExpr nx.Type = NameExprTypeDefine pkg.Predefine(false, n.Name) + pkg.UnassignableNames = append(pkg.UnassignableNames, n.Name) } case *FuncTypeExpr: for i := range n.Params { @@ -1442,6 +1443,8 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { if cx, ok := n.Func.(*ConstExpr); ok { fv := cx.GetFunc() if fv.PkgPath == uversePkgPath && fv.Name == "append" { + // append returns a slice and slices are always addressable. + n.Addressability = addressabilityStatusSatisfied if n.Varg && len(n.Args) == 2 { // If the second argument is a string, // convert to byteslice. @@ -1488,9 +1491,23 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { n.Args[1] = args1 } } + } else if fv.PkgPath == uversePkgPath && fv.Name == "new" { + // The pointer value returned is not addressable, but maybe some selector + // will make it addressable. For now mark it as not addressable. + n.Addressability = addressabilityStatusUnsatisfied } } + // If addressability is not satisfied at this point and the function call returns only one + // result, then mark addressability as unsatisfied. Otherwise, this expression has already + // been explicitly marked as satisfied, or the function returns multiple results, rendering + // addressability NotApplicable for this situation -- it should fallback to the error produced + // when trying to take a reference or slice the result of a call expression that returns + // multiple values. + if n.Addressability != addressabilityStatusSatisfied && len(ft.Results) == 1 { + n.Addressability = addressabilityStatusUnsatisfied + } + // Continue with general case. hasVarg := ft.HasVarg() isVarg := n.Varg @@ -1636,9 +1653,23 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // Replace const index with int *ConstExpr, // or if not const, assert integer type.. checkOrConvertIntegerKind(store, last, n, n.Index) + + // Addressability of this index expression can only be known for slice and + // strings, explanations below in the respective blocks. If this is an index + // on an array, do nothing. This will defer to the array's addresability when + // the `addressability` method is called on this index expression. + if dt.Kind() == SliceKind { + // A value at a slice index is always addressable because the underlying + // array is addressable. + n.Addressability = addressabilityStatusSatisfied + } else if dt.Kind() == StringKind { + // Special case; string indexes are never addressable. + n.Addressability = addressabilityStatusUnsatisfied + } case MapKind: mt := baseOf(gnoTypeOf(store, dt)).(*MapType) checkOrConvertType(store, last, n, &n.Index, mt.Key, false) + n.Addressability = addressabilityStatusUnsatisfied default: panic(fmt.Sprintf( "unexpected index base kind for type %s", @@ -1653,8 +1684,14 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { checkOrConvertIntegerKind(store, last, n, n.High) checkOrConvertIntegerKind(store, last, n, n.Max) - // if n.X is untyped, convert to corresponding type t := evalStaticTypeOf(store, last, n.X) + if t.Kind() == ArrayKind { + if n.X.addressability() == addressabilityStatusUnsatisfied { + panic(fmt.Sprintf("cannot take address of %s", n.X.String())) + } + } + + // if n.X is untyped, convert to corresponding type if isUntyped(t) { dt := defaultTypeOf(t) checkOrConvertType(store, last, n, &n.X, dt, false) @@ -1693,8 +1730,6 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { ) } - evalStaticType(store, last, n.Type) - // TRANS_LEAVE ----------------------- case *UnaryExpr: xt := evalStaticTypeOf(store, last, n.X) @@ -1754,6 +1789,10 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { convertType(store, last, n, &n.Elts[i].Key, IntType) checkOrConvertType(store, last, n, &n.Elts[i].Value, cclt.Elt, false) } + + // Slices are always addressable because the underlying array + // is added to the heap during initialization. + n.IsAddressable = true case *MapType: for i := 0; i < len(n.Elts); i++ { checkOrConvertType(store, last, n, &n.Elts[i].Key, cclt.Key, false) @@ -1793,6 +1832,16 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { } } + // If ftype is TRANS_REF_X, then this composite literal being created looks + // something like this in the code: `&MyStruct{}`. It is marked as addressable here + // because on TRANS_LEAVE for a RefExpr, it defers to the addressability of the + // expression it is referencing. When a composite literal is created with a preceding + // '&', it means the value is assigned to an address and that address is returned, + // so the value is addressable. + if ftype == TRANS_REF_X { + n.IsAddressable = true + } + // TRANS_LEAVE ----------------------- case *KeyValueExpr: // NOTE: For simplicity we just @@ -1809,6 +1858,9 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // TRANS_LEAVE ----------------------- case *SelectorExpr: xt := evalStaticTypeOf(store, last, n.X) + if xt.Kind() == PointerKind { + n.IsAddressable = true + } // Set selector path based on xt's type. switch cxt := xt.(type) { @@ -1821,6 +1873,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { panic(fmt.Sprintf("missing field %s in %s", n.Sel, cxt.String())) } + if len(tr) > 1 { // (the last vp, tr[len(tr)-1], is for n.Sel) if debug { @@ -1923,10 +1976,13 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // packages may contain constant vars, // so check and evaluate if so. tt := pn.GetStaticTypeOfAt(store, n.Path) - if isUntyped(tt) { + + // Produce a constant expression for both typed and untyped constants. + if isUntyped(tt) || pn.GetIsConstAt(store, n.Path) { cx := evalConst(store, last, n) return cx, TRANS_CONTINUE } + case *TypeType: // unbound method xt := evalStaticType(store, last, n.X) @@ -2001,6 +2057,15 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // TRANS_LEAVE ----------------------- case *AssignStmt: n.AssertCompatible(store, last) + if n.Op == ASSIGN { + for _, lh := range n.Lhs { + if ne, ok := lh.(*NameExpr); ok { + if !last.GetStaticBlock().IsAssignable(store, ne.Name) { + panic("not assignable") + } + } + } + } // NOTE: keep DEFINE and ASSIGN in sync. if n.Op == DEFINE { @@ -2378,9 +2443,20 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // We need to replace all references of the new // Type with old Type, including in attributes. n.Type.SetAttribute(ATTR_TYPE_VALUE, dst) - // Replace the type with *constTypeExpr{}, + // Replace the type with *{}, // otherwise methods would be un at runtime. n.Type = constType(n.Type, dst) + + case *RefExpr: + // If n.X is a RefExpr, then this expression is something like: + // &(&value). The resulting pointer value of the first reference is not + // addressable. Otherwise fall back to the target expression's addressability. + _, xIsRef := n.X.(*RefExpr) + tt := evalStaticTypeOf(store, last, n.X) + + if ft, is_func := tt.(*FuncType); (is_func && !ft.IsClosure) || xIsRef || n.X.addressability() == addressabilityStatusUnsatisfied { + panic(fmt.Sprintf("cannot take address of %s", n.X.String())) + } } // end type switch statement // END TRANS_LEAVE ----------------------- diff --git a/gnovm/pkg/gnolang/types.go b/gnovm/pkg/gnolang/types.go index 18774fcc462..374ac6d9150 100644 --- a/gnovm/pkg/gnolang/types.go +++ b/gnovm/pkg/gnolang/types.go @@ -1132,6 +1132,8 @@ type FuncType struct { typeid TypeID bound *FuncType + + IsClosure bool } // true for predefined func types that are not filled in yet. diff --git a/gnovm/stdlibs/crypto/sha256/sha256_test.gno b/gnovm/stdlibs/crypto/sha256/sha256_test.gno index 26d96cd547e..809f826f007 100644 --- a/gnovm/stdlibs/crypto/sha256/sha256_test.gno +++ b/gnovm/stdlibs/crypto/sha256/sha256_test.gno @@ -7,7 +7,8 @@ import ( ) func TestSha256Sum(t *testing.T) { - got := sha256.Sum256([]byte("sha256 this string"))[:] + result := sha256.Sum256([]byte("sha256 this string")) + got := result[:] expected := "1af1dfa857bf1d8814fe1af8983c18080019922e557f15a8a0d3db739d77aacb" if hex.EncodeToString(got) != expected { diff --git a/gnovm/tests/files/addressable_1.gno b/gnovm/tests/files/addressable_1.gno new file mode 100644 index 00000000000..30616145733 --- /dev/null +++ b/gnovm/tests/files/addressable_1.gno @@ -0,0 +1,38 @@ +package main + +func main() { + // Array pointers are addressable. + println(&getArrPtr1()[0]) + println(&getArrPtr2()[0]) + println(&getArrPtr3()[0]) + println(&new([1]int)[0]) + + // Array pointers are slicable. + println(getArrPtr1()[:]) + println(getArrPtr2()[:]) + println(getArrPtr3()[:]) + println(new([1]int)[:]) +} + +func getArrPtr1() *[1]int { + return &[1]int{1} +} + +func getArrPtr2() *[1]int { + a := [1]int{2} + return &a +} + +func getArrPtr3() *[1]int { + return new([1]int) +} + +// Output: +// &(1 int) +// &(2 int) +// &(0 int) +// &(0 int) +// slice[(1 int)] +// slice[(2 int)] +// slice[(0 int)] +// slice[(0 int)] diff --git a/gnovm/tests/files/addressable_10.gno b/gnovm/tests/files/addressable_10.gno new file mode 100644 index 00000000000..9052c172973 --- /dev/null +++ b/gnovm/tests/files/addressable_10.gno @@ -0,0 +1,12 @@ +package main + +type S struct { + i int +} + +func main() { + println(&new(S).i) +} + +// Output: +// &(0 int) diff --git a/gnovm/tests/files/addressable_10a_err.gno b/gnovm/tests/files/addressable_10a_err.gno new file mode 100644 index 00000000000..6781c6c67e5 --- /dev/null +++ b/gnovm/tests/files/addressable_10a_err.gno @@ -0,0 +1,14 @@ +package main + +func main() { + println(&getPtr()) +} + +type S struct{} + +func getPtr() *S { + return &S{} +} + +// Error: +// main/files/addressable_10a_err.gno:4:10: cannot take address of getPtr() diff --git a/gnovm/tests/files/addressable_10b_err.gno b/gnovm/tests/files/addressable_10b_err.gno new file mode 100644 index 00000000000..30048dfa8f6 --- /dev/null +++ b/gnovm/tests/files/addressable_10b_err.gno @@ -0,0 +1,12 @@ +package main + +type S struct { + i int +} + +func main() { + println(&new(S)) +} + +// Error: +// main/files/addressable_10b_err.gno:8:10: cannot take address of (const (new func(t type{})( *main.S)))(S) diff --git a/gnovm/tests/files/addressable_11.gno b/gnovm/tests/files/addressable_11.gno new file mode 100644 index 00000000000..607c155abfe --- /dev/null +++ b/gnovm/tests/files/addressable_11.gno @@ -0,0 +1,39 @@ +package main + +func main() { + var ii **int + i := new(int) + ii = &i + println(&(*ii)) + println(&ii) + println(i) + println(ii) + println(&i) + + j := new(int) + println(&(*j)) + + println(&(*getPtr())) + + derefTypeAssert() +} + +func getPtr() *int { + return new(int) +} + +func derefTypeAssert() { + var i interface{} + i = new(int) + println(&(*(i.(*int)))) +} + +// Output: +// &(&(0 int) *int) +// &(&(&(0 int) *int) **int) +// &(0 int) +// &(&(0 int) *int) +// &(&(0 int) *int) +// &(0 int) +// &(0 int) +// &(0 int) diff --git a/gnovm/tests/files/addressable_1a_err.gno b/gnovm/tests/files/addressable_1a_err.gno new file mode 100644 index 00000000000..a8c8c3b3c77 --- /dev/null +++ b/gnovm/tests/files/addressable_1a_err.gno @@ -0,0 +1,8 @@ +package main + +func main() { + _ = &[1]int{1}[0] +} + +// Error: +// main/files/addressable_1a_err.gno:4:6: cannot take address of [(const (1 int))](const-type int){(const (1 int))}[(const (0 int))] diff --git a/gnovm/tests/files/addressable_1b_err.gno b/gnovm/tests/files/addressable_1b_err.gno new file mode 100644 index 00000000000..9d8a7d12e4b --- /dev/null +++ b/gnovm/tests/files/addressable_1b_err.gno @@ -0,0 +1,8 @@ +package main + +func main() { + _ = [1]int{1}[:] +} + +// Error: +// main/files/addressable_1b_err.gno:4:6: cannot take address of [(const (1 int))](const-type int){(const (1 int))} diff --git a/gnovm/tests/files/addressable_1c_err.gno b/gnovm/tests/files/addressable_1c_err.gno new file mode 100644 index 00000000000..420531187df --- /dev/null +++ b/gnovm/tests/files/addressable_1c_err.gno @@ -0,0 +1,13 @@ +package main + +func main() { + _ = &getArr()[0] +} + +func getArr() [1]int { + arr := [1]int{1} + return arr +} + +// Error: +// main/files/addressable_1c_err.gno:4:6: cannot take address of getArr()[(const (0 int))] diff --git a/gnovm/tests/files/addressable_1d_err.gno b/gnovm/tests/files/addressable_1d_err.gno new file mode 100644 index 00000000000..56bb81c881f --- /dev/null +++ b/gnovm/tests/files/addressable_1d_err.gno @@ -0,0 +1,12 @@ +package main + +func main() { + _ = getArr()[:] +} + +func getArr() [1]int { + return [1]int{1} +} + +// Error: +// main/files/addressable_1d_err.gno:4:6: cannot take address of getArr() diff --git a/gnovm/tests/files/addressable_2.gno b/gnovm/tests/files/addressable_2.gno new file mode 100644 index 00000000000..1baf7f29e65 --- /dev/null +++ b/gnovm/tests/files/addressable_2.gno @@ -0,0 +1,40 @@ +package main + +func main() { + // Slices are addressable because the underlying array is addressable + // after slice initialization. + println(&[]int{1}[0]) + println(&getSlice1()[0]) + println(&getSlice2()[0]) + println(&[]int{1}) + + a := []int{1} + println(&append(a, 1, 2, 3, 4, 5)[5]) + + println([]int{1}[:]) + println(getSlice1()[:]) + println(getSlice2()[:]) + + b := []int{1} + println(append(b, 1, 2, 3, 4, 5)[:]) +} + +func getSlice1() []int { + return []int{2} +} + +func getSlice2() []int { + s := []int{3} + return s +} + +// Output: +// &(1 int) +// &(2 int) +// &(3 int) +// &(slice[(1 int)] []int) +// &(5 int) +// slice[(1 int)] +// slice[(2 int)] +// slice[(3 int)] +// slice[(1 int),(1 int),(2 int),(3 int),(4 int),(5 int)] diff --git a/gnovm/tests/files/addressable_2a_err.gno b/gnovm/tests/files/addressable_2a_err.gno new file mode 100644 index 00000000000..363fa3d2817 --- /dev/null +++ b/gnovm/tests/files/addressable_2a_err.gno @@ -0,0 +1,8 @@ +package main + +func main() { + _ = &[]int{1}[:] +} + +// Error: +// main/files/addressable_2a_err.gno:4:6: cannot take address of [](const-type int){(const (1 int))}[:] diff --git a/gnovm/tests/files/addressable_2b_err.gno b/gnovm/tests/files/addressable_2b_err.gno new file mode 100644 index 00000000000..eef7fd37ca5 --- /dev/null +++ b/gnovm/tests/files/addressable_2b_err.gno @@ -0,0 +1,12 @@ +package main + +func main() { + _ = &getSlice() +} + +func getSlice() []int { + return []int{1} +} + +// Error: +// main/files/addressable_2b_err.gno:4:6: cannot take address of getSlice() diff --git a/gnovm/tests/files/addressable_3.gno b/gnovm/tests/files/addressable_3.gno new file mode 100644 index 00000000000..34e9a2e1edc --- /dev/null +++ b/gnovm/tests/files/addressable_3.gno @@ -0,0 +1,105 @@ +package main + +type S struct { + i int + ip *int + a [1]int + ap *[1]int + s []int +} + +func main() { + intPtr := new(int) + *intPtr = 9 + + v1 := S{ + i: 4, + ip: intPtr, + a: [1]int{5}, + ap: new([1]int), + s: []int{6}, + } + + // v1 and all members are addressable. + println(&v1) + println(&v1.i) + println(*v1.ip) + println(&v1.a[0]) + println(&v1.ap[0]) + println(&v1.s[0]) + println("") + + // Defining a struct as a pointer also makes a member addressable. + println(&(&S{i: 4}).i) + println("") + + // Print only the members that are addressable when S is not addressable. + println(*S{ip: intPtr}.ip) + println(&S{ap: new([1]int)}.ap[0]) + println(&S{s: []int{6}}.s[0]) + println("") + + // A struct value returned by a function is not addressable. + // Only certain members are addressable. + println(*getStruct().ip) + println(&getStruct().ap[0]) + println(&getStruct().s[0]) + println("") + + // A struct pointer value returned by a function has all members addressable. + println(&getStructPtr().i) + println(*getStructPtr().ip) + println(&getStructPtr().a[0]) + println(&getStructPtr().ap[0]) + println(&getStructPtr().s[0]) +} + +func getStruct() S { + intPtr := new(int) + *intPtr = 9 + + return S{ + i: 4, + ip: intPtr, + a: [1]int{5}, + ap: new([1]int), + s: []int{6}, + } +} + +func getStructPtr() *S { + intPtr := new(int) + *intPtr = 9 + + return &S{ + i: 4, + ip: intPtr, + a: [1]int{5}, + ap: new([1]int), + s: []int{6}, + } +} + +// Output: +// &(struct{(4 int),(&(9 int) *int),(array[(5 int)] [1]int),(&(array[(0 int)] [1]int) *[1]int),(slice[(6 int)] []int)} main.S) +// &(4 int) +// 9 +// &(5 int) +// &(0 int) +// &(6 int) +// +// &(4 int) +// +// 9 +// &(0 int) +// &(6 int) +// +// 9 +// &(0 int) +// &(6 int) +// +// &(4 int) +// 9 +// &(5 int) +// &(0 int) +// &(6 int) diff --git a/gnovm/tests/files/addressable_3a_err.gno b/gnovm/tests/files/addressable_3a_err.gno new file mode 100644 index 00000000000..dcf3883e79b --- /dev/null +++ b/gnovm/tests/files/addressable_3a_err.gno @@ -0,0 +1,14 @@ +package main + +type S struct { + i int +} + +func main() { + // Can't take the address of non-addressable member of + // a non-addressable struct. + _ = &S{i: 4}.i +} + +// Error: +// main/files/addressable_3a_err.gno:10:6: cannot take address of S{i: (const (4 int))}.i diff --git a/gnovm/tests/files/addressable_3b_err.gno b/gnovm/tests/files/addressable_3b_err.gno new file mode 100644 index 00000000000..db4e810155a --- /dev/null +++ b/gnovm/tests/files/addressable_3b_err.gno @@ -0,0 +1,16 @@ +package main + +type S struct { + i int +} + +func main() { + _ = &getStruct().i +} + +func getStruct() S { + return S{i: 9} +} + +// Error: +// main/files/addressable_3b_err.gno:8:6: cannot take address of getStruct().i diff --git a/gnovm/tests/files/addressable_3c_err.gno b/gnovm/tests/files/addressable_3c_err.gno new file mode 100644 index 00000000000..536b5f77475 --- /dev/null +++ b/gnovm/tests/files/addressable_3c_err.gno @@ -0,0 +1,17 @@ +package main + +type MyStruct struct { + Mp *int +} + +func makeT() MyStruct { + x := 10 + return MyStruct{Mp: &x} +} + +func main() { + _ = &makeT().Mp +} + +// Error: +// main/files/addressable_3c_err.gno:13:6: cannot take address of makeT().Mp diff --git a/gnovm/tests/files/addressable_3d_err.gno b/gnovm/tests/files/addressable_3d_err.gno new file mode 100644 index 00000000000..00d75f80402 --- /dev/null +++ b/gnovm/tests/files/addressable_3d_err.gno @@ -0,0 +1,8 @@ +package main + +func main() { + _ = &(&struct{}{}) +} + +// Error: +// main/files/addressable_3d_err.gno:4:6: cannot take address of &(struct { }{}) diff --git a/gnovm/tests/files/addressable_4.gno b/gnovm/tests/files/addressable_4.gno new file mode 100644 index 00000000000..322cf8eabb3 --- /dev/null +++ b/gnovm/tests/files/addressable_4.gno @@ -0,0 +1,23 @@ +package main + +type S struct { + s string +} + +func main() { + // Strings are special; they are slicable even if not addressable. + println("hello"[:]) + println("hello"[0]) + println("hello"[2:4]) + println(S{s: "hello"}.s[:]) + println(S{s: "hello"}.s[0]) + println(S{s: "hello"}.s[2:4]) +} + +// Output: +// hello +// 104 +// ll +// hello +// 104 +// ll diff --git a/gnovm/tests/files/addressable_4a_err.gno b/gnovm/tests/files/addressable_4a_err.gno new file mode 100644 index 00000000000..17481976f3a --- /dev/null +++ b/gnovm/tests/files/addressable_4a_err.gno @@ -0,0 +1,9 @@ +package main + +func main() { + greeting := "hello" + _ = &greeting[2] +} + +// Error: +// main/files/addressable_4a_err.gno:5:6: cannot take address of greeting[(const (2 int))] diff --git a/gnovm/tests/files/addressable_5.gno b/gnovm/tests/files/addressable_5.gno new file mode 100644 index 00000000000..800cc744458 --- /dev/null +++ b/gnovm/tests/files/addressable_5.gno @@ -0,0 +1,11 @@ +package main + +import "encoding/binary" + +func main() { + // Verify that addressable results of expressions are + // still addressable when accessed via a selector. + var b []byte + le := &binary.LittleEndian + println(&le.AppendUint16(b, 0)[0]) +} diff --git a/gnovm/tests/files/addressable_5a_err_stdlibs.gno b/gnovm/tests/files/addressable_5a_err_stdlibs.gno new file mode 100644 index 00000000000..bc6318f511d --- /dev/null +++ b/gnovm/tests/files/addressable_5a_err_stdlibs.gno @@ -0,0 +1,11 @@ +package main + +import "math" + +func main() { + // Untyped constants are not addressable. + _ = &math.MaxUint8 +} + +// Error: +// main/files/addressable_5a_err_stdlibs.gno:7:6: cannot take address of (const (255 bigint)) diff --git a/gnovm/tests/files/addressable_5b_err_stdlibs.gno b/gnovm/tests/files/addressable_5b_err_stdlibs.gno new file mode 100644 index 00000000000..e2028a22c9e --- /dev/null +++ b/gnovm/tests/files/addressable_5b_err_stdlibs.gno @@ -0,0 +1,11 @@ +package main + +import "std" + +func main() { + // Type constants are not addressable. + _ = &std.BankerTypeReadonly +} + +// Error: +// main/files/addressable_5b_err_stdlibs.gno:7:6: cannot take address of (const (0 std.BankerType)) diff --git a/gnovm/tests/files/addressable_5c_err.gno b/gnovm/tests/files/addressable_5c_err.gno new file mode 100644 index 00000000000..92de1aeb30a --- /dev/null +++ b/gnovm/tests/files/addressable_5c_err.gno @@ -0,0 +1,10 @@ +package main + +const a = 1 + +func main() { + _ = &a +} + +// Error: +// main/files/addressable_5c_err.gno:6:6: cannot take address of (const (1 bigint)) diff --git a/gnovm/tests/files/addressable_5d_err.gno b/gnovm/tests/files/addressable_5d_err.gno new file mode 100644 index 00000000000..fe78ed36681 --- /dev/null +++ b/gnovm/tests/files/addressable_5d_err.gno @@ -0,0 +1,10 @@ +package main + +const a int = 1 + +func main() { + _ = &a +} + +// Error: +// main/files/addressable_5d_err.gno:6:6: cannot take address of (const (1 int)) diff --git a/gnovm/tests/files/addressable_6.gno b/gnovm/tests/files/addressable_6.gno new file mode 100644 index 00000000000..9fc0616f980 --- /dev/null +++ b/gnovm/tests/files/addressable_6.gno @@ -0,0 +1,27 @@ +package main + +type S struct { + a int +} + +type Alias []int + +func main() { + // Type assertions copy the value being asserted, so only pointers and + // slices are addressable. Slices are addressable because a copy of a slice + // maintains a reference to the same underlying array. + var i interface{} + i = []int{1} + println(&i.([]int)[0]) + + i = &S{} + println(&i.(*S).a) + + i = Alias{4} + println(&i.(Alias)[0]) +} + +// Output: +// &(1 int) +// &(0 int) +// &(4 int) diff --git a/gnovm/tests/files/addressable_6a_err.gno b/gnovm/tests/files/addressable_6a_err.gno new file mode 100644 index 00000000000..f27432fa81b --- /dev/null +++ b/gnovm/tests/files/addressable_6a_err.gno @@ -0,0 +1,10 @@ +package main + +func main() { + var i interface{} + i = 5 + println(&i.(int)) +} + +// Error: +// main/files/addressable_6a_err.gno:6:10: cannot take address of i.((const-type int)) diff --git a/gnovm/tests/files/addressable_6b_err.gno b/gnovm/tests/files/addressable_6b_err.gno new file mode 100644 index 00000000000..ea2e19aa3c4 --- /dev/null +++ b/gnovm/tests/files/addressable_6b_err.gno @@ -0,0 +1,14 @@ +package main + +type S struct { + a int +} + +func main() { + var i interface{} + i = S{a: 9} + println(&i.(S).a) +} + +// Error: +// main/files/addressable_6b_err.gno:10:10: cannot take address of i.(S).a diff --git a/gnovm/tests/files/addressable_6c_err.gno b/gnovm/tests/files/addressable_6c_err.gno new file mode 100644 index 00000000000..03666922eee --- /dev/null +++ b/gnovm/tests/files/addressable_6c_err.gno @@ -0,0 +1,10 @@ +package main + +func main() { + var i interface{} + i = [1]int{1} + println(&i.([1]int)[0]) +} + +// Error: +// main/files/addressable_6c_err.gno:6:10: cannot take address of i.([(const (1 int))](const-type int))[(const (0 int))] diff --git a/gnovm/tests/files/addressable_6d_err.gno b/gnovm/tests/files/addressable_6d_err.gno new file mode 100644 index 00000000000..b6058b7c024 --- /dev/null +++ b/gnovm/tests/files/addressable_6d_err.gno @@ -0,0 +1,12 @@ +package main + +func main() { + _ = &getSlice().([]int) +} + +func getSlice() interface{} { + return []int{1} +} + +// Error: +// main/files/addressable_6d_err.gno:4:6: cannot take address of getSlice().([](const-type int)) diff --git a/gnovm/tests/files/addressable_7a_err.gno b/gnovm/tests/files/addressable_7a_err.gno new file mode 100644 index 00000000000..f3648b6b990 --- /dev/null +++ b/gnovm/tests/files/addressable_7a_err.gno @@ -0,0 +1,12 @@ +package main + +func foo() ([]int, []string) { + return []int{1, 2, 3}, []string{"a", "b", "c"} +} + +func main() { + _ = &foo() +} + +// Error: +// main/files/addressable_7a_err.gno:8:2: getTypeOf() only supports *CallExpr with 1 result, got ([]int,[]string) diff --git a/gnovm/tests/files/addressable_7b_err.gno b/gnovm/tests/files/addressable_7b_err.gno new file mode 100644 index 00000000000..a621d688ea4 --- /dev/null +++ b/gnovm/tests/files/addressable_7b_err.gno @@ -0,0 +1,8 @@ +package main + +func main() { + _, _ = &int(9) +} + +// Error: +// main/files/addressable_7b_err.gno:4:9: cannot take address of (const (9 int)) diff --git a/gnovm/tests/files/addressable_8.gno b/gnovm/tests/files/addressable_8.gno new file mode 100644 index 00000000000..3fde1018185 --- /dev/null +++ b/gnovm/tests/files/addressable_8.gno @@ -0,0 +1,9 @@ +package main + +func main() { + f := func() { println("Hello, World!") } + println(&f) +} + +// Output: +// &(func()(){...} func()()) diff --git a/gnovm/tests/files/addressable_8a_err.gno b/gnovm/tests/files/addressable_8a_err.gno new file mode 100644 index 00000000000..6e3395da773 --- /dev/null +++ b/gnovm/tests/files/addressable_8a_err.gno @@ -0,0 +1,8 @@ +package main + +func main() { + _ = &func() { println("Hello, World!") } +} + +// Error: +// main/files/addressable_8a_err.gno:4:6: cannot take address of func func(){ (const (println func(xs ...interface{})()))((const ("Hello, World!" string))) } diff --git a/gnovm/tests/files/addressable_9.gno b/gnovm/tests/files/addressable_9.gno new file mode 100644 index 00000000000..3b0b832f781 --- /dev/null +++ b/gnovm/tests/files/addressable_9.gno @@ -0,0 +1,32 @@ +package main + +type S struct { + i int + t *T +} + +type T struct { + i int +} + +func main() { + m := map[int]*S{} + s := &S{i: 4} + m[5] = s + println(&m[5].i) + + mm := map[int]S{} + ss := S{t: new(T)} + mm[8] = ss + println(&mm[8].t.i) + + mmm := map[int]map[int]*S{} + mmm[3] = map[int]*S{} + mmm[3][3] = &S{i: 7} + println(&mmm[3][3].i) +} + +// Output: +// &(4 int) +// &(0 int) +// &(7 int) diff --git a/gnovm/tests/files/addressable_9a_err.gno b/gnovm/tests/files/addressable_9a_err.gno new file mode 100644 index 00000000000..038990e1c01 --- /dev/null +++ b/gnovm/tests/files/addressable_9a_err.gno @@ -0,0 +1,10 @@ +package main + +func main() { + m := map[int]int{} + m[4] = 5 + println(&m[4]) +} + +// Error: +// main/files/addressable_9a_err.gno:6:10: cannot take address of m[(const (4 int))] diff --git a/gnovm/tests/files/addressable_9b_err.gno b/gnovm/tests/files/addressable_9b_err.gno new file mode 100644 index 00000000000..92db1fbeefc --- /dev/null +++ b/gnovm/tests/files/addressable_9b_err.gno @@ -0,0 +1,15 @@ +package main + +type S struct { + i int +} + +func main() { + mmm := map[int]map[int]S{} + mmm[3] = map[int]S{} + mmm[3][3] = S{i: 7} + println(&mmm[3][3].i) +} + +// Error: +// main/files/addressable_9b_err.gno:11:10: cannot take address of mmm[(const (3 int))][(const (3 int))].i From d95ee0493f0d12ab538de19eb97d3b175ca9c246 Mon Sep 17 00:00:00 2001 From: Morgan Date: Mon, 20 Jan 2025 17:26:21 +0100 Subject: [PATCH 10/12] test: fix TestDownloadDeps after #3534 (#3565) --- gnovm/cmd/gno/download_deps_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gnovm/cmd/gno/download_deps_test.go b/gnovm/cmd/gno/download_deps_test.go index 0828e9b2245..15c344cbd28 100644 --- a/gnovm/cmd/gno/download_deps_test.go +++ b/gnovm/cmd/gno/download_deps_test.go @@ -46,9 +46,10 @@ func TestDownloadDeps(t *testing.T) { }, }, }, - requirements: []string{"avl"}, + requirements: []string{"avl", "ufmt"}, ioErrContains: []string{ "gno: downloading gno.land/p/demo/avl", + "gno: downloading gno.land/p/demo/ufmt", }, }, { desc: "fetch_gno.land/p/demo/blog6", @@ -80,9 +81,10 @@ func TestDownloadDeps(t *testing.T) { New: module.Version{Path: "gno.land/p/demo/avl"}, }}, }, - requirements: []string{"avl"}, + requirements: []string{"avl", "ufmt"}, ioErrContains: []string{ "gno: downloading gno.land/p/demo/avl", + "gno: downloading gno.land/p/demo/ufmt", }, }, { desc: "fetch_replace_local", From 8702da906f177cd10e12133286844018e15a0307 Mon Sep 17 00:00:00 2001 From: Antoine Eddi <5222525+aeddi@users.noreply.github.com> Date: Mon, 20 Jan 2025 17:26:37 +0100 Subject: [PATCH 11/12] chore: fix gnohealth command/subcommand help (#3555) This PR fixes a detail, the help descriptions of the gnohealth command that are incorrect. --- contribs/gnohealth/health.go | 24 ------------------- .../gnohealth/internal/timestamp/timestamp.go | 2 +- contribs/gnohealth/main.go | 15 +++++++++++- 3 files changed, 15 insertions(+), 26 deletions(-) delete mode 100644 contribs/gnohealth/health.go diff --git a/contribs/gnohealth/health.go b/contribs/gnohealth/health.go deleted file mode 100644 index 5118cac5fa5..00000000000 --- a/contribs/gnohealth/health.go +++ /dev/null @@ -1,24 +0,0 @@ -package main - -import ( - "github.com/gnolang/gno/contribs/gnohealth/internal/timestamp" - "github.com/gnolang/gno/tm2/pkg/commands" -) - -func newHealthCmd(io commands.IO) *commands.Command { - cmd := commands.NewCommand( - commands.Metadata{ - ShortUsage: " [flags] [...]", - ShortHelp: "gno health check suite", - LongHelp: "Gno health check suite, to verify that different parts of Gno are working correctly", - }, - commands.NewEmptyConfig(), - commands.HelpExec, - ) - - cmd.AddSubCommands( - timestamp.NewTimestampCmd(io), - ) - - return cmd -} diff --git a/contribs/gnohealth/internal/timestamp/timestamp.go b/contribs/gnohealth/internal/timestamp/timestamp.go index 50521b9130f..cb5dab44f91 100644 --- a/contribs/gnohealth/internal/timestamp/timestamp.go +++ b/contribs/gnohealth/internal/timestamp/timestamp.go @@ -35,7 +35,7 @@ func NewTimestampCmd(io commands.IO) *commands.Command { return commands.NewCommand( commands.Metadata{ Name: "timestamp", - ShortUsage: "[flags]", + ShortUsage: "timestamp [flags]", ShortHelp: "check if block timestamps are drifting", LongHelp: "This command checks if block timestamps are drifting on a blockchain by connecting to a specified node via RPC.", }, diff --git a/contribs/gnohealth/main.go b/contribs/gnohealth/main.go index 4325c657976..a4a23369710 100644 --- a/contribs/gnohealth/main.go +++ b/contribs/gnohealth/main.go @@ -4,11 +4,24 @@ import ( "context" "os" + "github.com/gnolang/gno/contribs/gnohealth/internal/timestamp" "github.com/gnolang/gno/tm2/pkg/commands" ) func main() { - cmd := newHealthCmd(commands.NewDefaultIO()) + cmd := commands.NewCommand( + commands.Metadata{ + ShortUsage: " [flags]", + LongHelp: "Gno health check suite, to verify that different parts of Gno are working correctly", + }, + commands.NewEmptyConfig(), + commands.HelpExec, + ) + + io := commands.NewDefaultIO() + cmd.AddSubCommands( + timestamp.NewTimestampCmd(io), + ) cmd.Execute(context.Background(), os.Args[1:]) } From 6f37334052741ec38ad8fa097dd95b140a605fa2 Mon Sep 17 00:00:00 2001 From: Stefan Nikolic Date: Tue, 21 Jan 2025 11:17:16 +0100 Subject: [PATCH 12/12] =?UTF-8?q?fix(grc721):=20Make=20basic=20`metadataNF?= =?UTF-8?q?T`=20implementation=20usable=20from=20external=C2=A0packages=20?= =?UTF-8?q?(#3495)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The current implementation of `grc721_metadata.gno` has a critical usability issue where external packages can't access `basic_nft.gno` methods when using `NewNFTWithMetadata()`. This is because: 1. The `metadataNFT` struct in `grc721_metadata.gno` embeds `*basicNFT` which is unexported 2. While embedding promotes methods within the same package, unexported methods are not accessible from external packages 3. External packages could only access the metadata-specific methods (`SetTokenMetadata`, `TokenMetadata`) Changes made: - Add forwarding methods in `metadataNFT` for all `basicNFT` functionality (`Name`, `Symbol`, `BalanceOf`, etc.) - Remove owner check in `SetTokenMetadata`, since this is something that should be left for NFT collection creator to choose who should be able to use this method These changes ensure that external packages can properly use the metadata NFT implementation with full NFT functionality while maintaining proper encapsulation of the underlying `basicNFT` struct interface. An example of a working usage of this updated `grc721_metadata.gno` can be found in [this file](https://github.com/gnolang/gno/pull/3344/files#diff-b5a138a5b86103af5764def3e87ec3814c9822e5b6da0c705b732273edbde57b). --------- Co-authored-by: Leon Hudak <33522493+leohhhn@users.noreply.github.com> --- .../p/demo/grc/grc721/grc721_metadata.gno | 102 ++++++++++-------- 1 file changed, 60 insertions(+), 42 deletions(-) diff --git a/examples/gno.land/p/demo/grc/grc721/grc721_metadata.gno b/examples/gno.land/p/demo/grc/grc721/grc721_metadata.gno index 05fad41be18..2b408a206ed 100644 --- a/examples/gno.land/p/demo/grc/grc721/grc721_metadata.gno +++ b/examples/gno.land/p/demo/grc/grc721/grc721_metadata.gno @@ -29,16 +29,6 @@ func NewNFTWithMetadata(name string, symbol string) *metadataNFT { // SetTokenMetadata sets metadata for a given token ID. func (s *metadataNFT) SetTokenMetadata(tid TokenID, metadata Metadata) error { - // Check if the caller is the owner of the token - owner, err := s.basicNFT.OwnerOf(tid) - if err != nil { - return err - } - caller := std.PrevRealm().Addr() - if caller != owner { - return ErrCallerIsNotOwner - } - // Set the metadata for the token ID in the extensions AVL tree s.extensions.Set(string(tid), metadata) return nil @@ -55,44 +45,72 @@ func (s *metadataNFT) TokenMetadata(tid TokenID) (Metadata, error) { return metadata.(Metadata), nil } -// mint mints a new token and assigns it to the specified address. -func (s *metadataNFT) mint(to std.Address, tid TokenID) error { - // Check if the address is valid - if err := isValidAddress(to); err != nil { - return err - } +// Basic NFT methods forwarded to embedded basicNFT - // Check if the token ID already exists - if s.basicNFT.exists(tid) { - return ErrTokenIdAlreadyExists - } +func (s *metadataNFT) Name() string { + return s.basicNFT.Name() +} - s.basicNFT.beforeTokenTransfer(zeroAddress, to, tid, 1) +func (s *metadataNFT) Symbol() string { + return s.basicNFT.Symbol() +} - // Check if the token ID was minted by beforeTokenTransfer - if s.basicNFT.exists(tid) { - return ErrTokenIdAlreadyExists - } +func (s *metadataNFT) TokenCount() uint64 { + return s.basicNFT.TokenCount() +} - // Increment balance of the recipient address - toBalance, err := s.basicNFT.BalanceOf(to) - if err != nil { - return err - } - toBalance += 1 - s.basicNFT.balances.Set(to.String(), toBalance) +func (s *metadataNFT) BalanceOf(addr std.Address) (uint64, error) { + return s.basicNFT.BalanceOf(addr) +} + +func (s *metadataNFT) OwnerOf(tid TokenID) (std.Address, error) { + return s.basicNFT.OwnerOf(tid) +} - // Set owner of the token ID to the recipient address - s.basicNFT.owners.Set(string(tid), to) +func (s *metadataNFT) TokenURI(tid TokenID) (string, error) { + return s.basicNFT.TokenURI(tid) +} - std.Emit( - TransferEvent, - "from", string(zeroAddress), - "to", string(to), - "tokenId", string(tid), - ) +func (s *metadataNFT) SetTokenURI(tid TokenID, tURI TokenURI) (bool, error) { + return s.basicNFT.SetTokenURI(tid, tURI) +} - s.basicNFT.afterTokenTransfer(zeroAddress, to, tid, 1) +func (s *metadataNFT) IsApprovedForAll(owner, operator std.Address) bool { + return s.basicNFT.IsApprovedForAll(owner, operator) +} - return nil +func (s *metadataNFT) Approve(to std.Address, tid TokenID) error { + return s.basicNFT.Approve(to, tid) +} + +func (s *metadataNFT) GetApproved(tid TokenID) (std.Address, error) { + return s.basicNFT.GetApproved(tid) +} + +func (s *metadataNFT) SetApprovalForAll(operator std.Address, approved bool) error { + return s.basicNFT.SetApprovalForAll(operator, approved) +} + +func (s *metadataNFT) SafeTransferFrom(from, to std.Address, tid TokenID) error { + return s.basicNFT.SafeTransferFrom(from, to, tid) +} + +func (s *metadataNFT) TransferFrom(from, to std.Address, tid TokenID) error { + return s.basicNFT.TransferFrom(from, to, tid) +} + +func (s *metadataNFT) Mint(to std.Address, tid TokenID) error { + return s.basicNFT.Mint(to, tid) +} + +func (s *metadataNFT) SafeMint(to std.Address, tid TokenID) error { + return s.basicNFT.SafeMint(to, tid) +} + +func (s *metadataNFT) Burn(tid TokenID) error { + return s.basicNFT.Burn(tid) +} + +func (s *metadataNFT) RenderHome() string { + return s.basicNFT.RenderHome() }