diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.md b/.github/ISSUE_TEMPLATE/BUG-REPORT.md index 70a20a4c47e..a63b450d678 100644 --- a/.github/ISSUE_TEMPLATE/BUG-REPORT.md +++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.md @@ -1,6 +1,7 @@ --- name: Bug Report Template about: Create a bug report +labels: "🐞 bug" # NOTE: keep in sync with gnovm/cmd/gno/bug.go --- diff --git a/.github/workflows/fossa.yml b/.github/workflows/fossa.yml index 9de8d536b29..f9d3110ba82 100644 --- a/.github/workflows/fossa.yml +++ b/.github/workflows/fossa.yml @@ -31,7 +31,7 @@ jobs: uses: coursier/cache-action@v6.4.6 - name: Set up JDK 17 - uses: coursier/setup-action@v1.3.6 + uses: coursier/setup-action@v1.3.8 with: jvm: temurin:1.17 diff --git a/.github/workflows/releaser-master.yml b/.github/workflows/releaser-master.yml index 7f81ef1ad1a..eb5698e9d8f 100644 --- a/.github/workflows/releaser-master.yml +++ b/.github/workflows/releaser-master.yml @@ -27,7 +27,7 @@ jobs: cache: true - uses: sigstore/cosign-installer@v3.7.0 - - uses: anchore/sbom-action/download-syft@v0.17.5 + - uses: anchore/sbom-action/download-syft@v0.17.7 - uses: docker/login-action@v3 with: diff --git a/.github/workflows/releaser-nightly.yml b/.github/workflows/releaser-nightly.yml index fd4eaa86b0e..aed56526a2f 100644 --- a/.github/workflows/releaser-nightly.yml +++ b/.github/workflows/releaser-nightly.yml @@ -24,7 +24,7 @@ jobs: cache: true - uses: sigstore/cosign-installer@v3.7.0 - - uses: anchore/sbom-action/download-syft@v0.17.5 + - uses: anchore/sbom-action/download-syft@v0.17.7 - uses: docker/login-action@v3 with: diff --git a/.github/workflows/releaser.yml b/.github/workflows/releaser.yml index 8bbc9323cad..aeda7ed2c7e 100644 --- a/.github/workflows/releaser.yml +++ b/.github/workflows/releaser.yml @@ -24,7 +24,7 @@ jobs: cache: true - uses: sigstore/cosign-installer@v3.7.0 - - uses: anchore/sbom-action/download-syft@v0.17.5 + - uses: anchore/sbom-action/download-syft@v0.17.7 - uses: docker/login-action@v3 with: diff --git a/docs/reference/gno-js-client/gno-provider.md b/docs/reference/gno-js-client/gno-provider.md index 1b9cbd53652..c76bfebfe31 100644 --- a/docs/reference/gno-js-client/gno-provider.md +++ b/docs/reference/gno-js-client/gno-provider.md @@ -7,6 +7,38 @@ id: gno-js-provider The `Gno Provider` is an extension on the `tm2-js-client` `Provider`, outlined [here](../tm2-js-client/Provider/provider.md). Both JSON-RPC and WS providers are included with the package. +## Instantiation + +### new GnoWSProvider + +Creates a new instance of the Gno WebSocket Provider, based on [`tm2-js-client` `WSProvider`](../tm2-js-client/Provider/ws-provider.md). + +#### Parameters + +Same as [`tm2-js-client` `WSProvider`](../tm2-js-client/Provider/ws-provider.md). + +#### Usage + +```ts +new GnoWSProvider('ws://staging.gno.land:26657/ws'); +// provider with WS connection is created +``` + +### new GnoJSONRPCProvider + +Creates a new instance of the Gno JSON-RPC Provider, based on [`tm2-js-client` `JSONRPCProvider`](../tm2-js-client/Provider/json-rpc-provider.md). + +#### Parameters + +Same as [`tm2-js-client` `JSONRPCProvider`](../tm2-js-client/Provider/json-rpc-provider.md). + +#### Usage + +```ts +new GnoJSONRPCProvider('http://staging.gno.land:36657'); +// provider is created +``` + ## Realm Methods ### getRenderOutput diff --git a/examples/README.md b/examples/README.md index b112e564d13..758f0f586e5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,21 +1,37 @@ -# Gnolang examples +# Examples -This folder showcases Gnolang realms and library demos. These examples not only aid in engine testing but also provide a glimpse into the potential of Gnolang's capabilities. +This folder showcases example Gno realms (smart contracts) and pure packages (libraries). +These examples provide a glimpse into the potential of gno.land and the capabilities of Gno, +while also serving as a test suite for the GnoVM. -While sharing contracts here can enhance engine testing, it's not mandatory. If considering a separate repository for contracts, be aware that this might restrict the experience due to the continuous efforts around `gno mod` support. A key point to note is that the main repository cannot reference separate code, which might pose developmental challenges. +Pure packages and realms in this folder are pre-deployed to gno.land testnets, +making them readily available for on-chain use. However, **there is no guarantee +that the code is bug-free, so it should be used with caution and an understanding of potential risks.** -## Personal Realms & Shared Content - -**Prioritizing Shared Content:** As we expand our examples and use-cases, it's essential to prioritize shared content that benefits the broader community. These examples serve as a foundation and reference for all users. - -**Personal Realms Inclusion:** We're open to personal realms, but they must exemplify best practices and inspire others. To maintain our repository's organization, we may decline some realms. If so, consider uploading onchain and keeping source code separately. For higher acceptance odds, offer useful or original examples. +## Structure -**Recommended Approach:** -- Use `r/demo` and `p/demo` for generic examples and components that can be imported by others. These are meant to be easily referenced and utilized by the community. -- Personal realms are welcomed if they are easily maintainable with the Continuous Integration (CI) system. If a personal realm becomes cumbersome to maintain or doesn't align with the CI's checks, it might be relocated to a less prominent location or even removed. +This folder mimics the gno.land package path system; the "root" of the system is +the `gno.land` folder. Next, it branches out to `p/` and `r/`, which contain +pure packages and realms, respectively. -## Usage - -Our recommendation is to use the [gno](../gnovm/cmd/gno) utility to develop contracts locally before publishing them on-chain. This approach offers a faster and streamlined workflow, along with additional debugging features. Simply fork or create new contracts and refer to the Makefile. Once everything looks good locally, you can then publish it on a localnet or testnet. +## Personal Realms & Shared Content -For further guidance and insights, please refer to the [`awesome-gno` tutorials](https://github.com/gnolang/awesome-gno#tutorials). +**Prioritizing Shared Content:** As we expand our examples and use-cases, it's +essential to prioritize shared content that benefits the broader community. +These examples serve as a foundation and reference for all users. + +**Personal Realms & Pure Packages:** We welcome personal realms that +exemplify best practices and inspire others. To maintain the organization +of the monorepo, some submissions may be declined. If so, consider uploading +[permissionlessly](../docs/gno-tooling/cli/gnokey/state-changing-calls.md#addpackage) +and storing the source code in a separate repo. For higher +acceptance odds, offer useful and original examples. + +**Recommended Approach:** +- Use `r/demo` and `p/demo` for generic examples and components that can be + imported by others. These are meant to be easily referenced and utilized by the + community. +- Packages under personal namespaces, such as in [r/leon](./gno.land/r/leon), + are welcome if they are easily maintainable with the Continuous Integration (CI) + system. If a personal realm becomes cumbersome to maintain or doesn't align with + the CI's checks, it might be relocated to a less prominent location or even removed. \ No newline at end of file diff --git a/examples/gno.land/p/demo/fqname/fqname.gno b/examples/gno.land/p/demo/fqname/fqname.gno index 8cccdb9e8b7..07d9e4b4621 100644 --- a/examples/gno.land/p/demo/fqname/fqname.gno +++ b/examples/gno.land/p/demo/fqname/fqname.gno @@ -4,7 +4,9 @@ // package-level declaration. package fqname -import "strings" +import ( + "strings" +) // Parse splits a fully qualified identifier into its package path and name // components. It handles cases with and without slashes in the package path. @@ -63,10 +65,13 @@ func RenderLink(pkgPath, slug string) string { if slug != "" { return "[" + pkgPath + "](" + pkgLink + ")." + slug } + return "[" + pkgPath + "](" + pkgLink + ")" } + if slug != "" { return pkgPath + "." + slug } + return pkgPath } diff --git a/examples/gno.land/p/demo/ownable/ownable.gno b/examples/gno.land/p/demo/ownable/ownable.gno index a77b22461a9..48a1c15fffa 100644 --- a/examples/gno.land/p/demo/ownable/ownable.gno +++ b/examples/gno.land/p/demo/ownable/ownable.gno @@ -37,8 +37,8 @@ func (o *Ownable) TransferOwnership(newOwner std.Address) error { o.owner = newOwner std.Emit( OwnershipTransferEvent, - "from", string(prevOwner), - "to", string(newOwner), + "from", prevOwner.String(), + "to", newOwner.String(), ) return nil @@ -58,7 +58,7 @@ func (o *Ownable) DropOwnership() error { std.Emit( OwnershipTransferEvent, - "from", string(prevOwner), + "from", prevOwner.String(), "to", "", ) diff --git a/examples/gno.land/p/demo/pausable/pausable.gno b/examples/gno.land/p/demo/pausable/pausable.gno index eae3456ba61..e9cce63c1e3 100644 --- a/examples/gno.land/p/demo/pausable/pausable.gno +++ b/examples/gno.land/p/demo/pausable/pausable.gno @@ -1,6 +1,10 @@ package pausable -import "gno.land/p/demo/ownable" +import ( + "std" + + "gno.land/p/demo/ownable" +) type Pausable struct { *ownable.Ownable @@ -35,6 +39,8 @@ func (p *Pausable) Pause() error { } p.paused = true + std.Emit("Paused", "account", p.Owner().String()) + return nil } @@ -45,5 +51,7 @@ func (p *Pausable) Unpause() error { } p.paused = false + std.Emit("Unpaused", "account", p.Owner().String()) + return nil } diff --git a/examples/gno.land/p/moul/mdtable/gno.mod b/examples/gno.land/p/moul/mdtable/gno.mod new file mode 100644 index 00000000000..0cea0458895 --- /dev/null +++ b/examples/gno.land/p/moul/mdtable/gno.mod @@ -0,0 +1,3 @@ +module gno.land/p/moul/mdtable + +require gno.land/p/demo/urequire v0.0.0-latest diff --git a/examples/gno.land/p/moul/mdtable/mdtable.gno b/examples/gno.land/p/moul/mdtable/mdtable.gno new file mode 100644 index 00000000000..13812bd973d --- /dev/null +++ b/examples/gno.land/p/moul/mdtable/mdtable.gno @@ -0,0 +1,66 @@ +// Package mdtable provides a simple way to create Markdown tables. +// +// Example usage: +// +// import "gno.land/p/moul/mdtable" +// +// func Render(path string) string { +// table := mdtable.Table{ +// Headers: []string{"ID", "Title", "Status", "Date"}, +// } +// table.Append([]string{"#1", "Add a new validator", "succeed", "2024-01-01"}) +// table.Append([]string{"#2", "Change parameter", "timed out", "2024-01-02"}) +// return table.String() +// } +// +// Output: +// +// | ID | Title | Status | Date | +// | --- | --- | --- | --- | +// | #1 | Add a new validator | succeed | 2024-01-01 | +// | #2 | Change parameter | timed out | 2024-01-02 | +package mdtable + +import ( + "strings" +) + +type Table struct { + Headers []string + Rows [][]string + // XXX: optional headers alignment. +} + +func (t *Table) Append(row []string) { + t.Rows = append(t.Rows, row) +} + +func (t Table) String() string { + // XXX: switch to using text/tabwriter when porting to Gno to support + // better-formatted raw Markdown output. + + if len(t.Headers) == 0 && len(t.Rows) == 0 { + return "" + } + + var sb strings.Builder + + if len(t.Headers) == 0 { + t.Headers = make([]string, len(t.Rows[0])) + } + + // Print header. + sb.WriteString("| " + strings.Join(t.Headers, " | ") + " |\n") + sb.WriteString("|" + strings.Repeat(" --- |", len(t.Headers)) + "\n") + + // Print rows. + for _, row := range t.Rows { + escapedRow := make([]string, len(row)) + for i, cell := range row { + escapedRow[i] = strings.ReplaceAll(cell, "|", "|") // Escape pipe characters. + } + sb.WriteString("| " + strings.Join(escapedRow, " | ") + " |\n") + } + + return sb.String() +} diff --git a/examples/gno.land/p/moul/mdtable/mdtable_test.gno b/examples/gno.land/p/moul/mdtable/mdtable_test.gno new file mode 100644 index 00000000000..87836a3ab11 --- /dev/null +++ b/examples/gno.land/p/moul/mdtable/mdtable_test.gno @@ -0,0 +1,158 @@ +package mdtable_test + +import ( + "testing" + + "gno.land/p/demo/urequire" + "gno.land/p/moul/mdtable" +) + +// XXX: switch to `func Example() {}` when supported. +func TestExample(t *testing.T) { + table := mdtable.Table{ + Headers: []string{"ID", "Title", "Status"}, + Rows: [][]string{ + {"#1", "Add a new validator", "succeed"}, + {"#2", "Change parameter", "timed out"}, + {"#3", "Fill pool", "active"}, + }, + } + + got := table.String() + expected := `| ID | Title | Status | +| --- | --- | --- | +| #1 | Add a new validator | succeed | +| #2 | Change parameter | timed out | +| #3 | Fill pool | active | +` + + urequire.Equal(t, got, expected) +} + +func TestTableString(t *testing.T) { + tests := []struct { + name string + table mdtable.Table + expected string + }{ + { + name: "With Headers and Rows", + table: mdtable.Table{ + Headers: []string{"ID", "Title", "Status", "Date"}, + Rows: [][]string{ + {"#1", "Add a new validator", "succeed", "2024-01-01"}, + {"#2", "Change parameter", "timed out", "2024-01-02"}, + }, + }, + expected: `| ID | Title | Status | Date | +| --- | --- | --- | --- | +| #1 | Add a new validator | succeed | 2024-01-01 | +| #2 | Change parameter | timed out | 2024-01-02 | +`, + }, + { + name: "Without Headers", + table: mdtable.Table{ + Rows: [][]string{ + {"#1", "Add a new validator", "succeed", "2024-01-01"}, + {"#2", "Change parameter", "timed out", "2024-01-02"}, + }, + }, + expected: `| | | | | +| --- | --- | --- | --- | +| #1 | Add a new validator | succeed | 2024-01-01 | +| #2 | Change parameter | timed out | 2024-01-02 | +`, + }, + { + name: "Without Rows", + table: mdtable.Table{ + Headers: []string{"ID", "Title", "Status", "Date"}, + }, + expected: `| ID | Title | Status | Date | +| --- | --- | --- | --- | +`, + }, + { + name: "With Pipe Character in Content", + table: mdtable.Table{ + Headers: []string{"ID", "Title", "Status", "Date"}, + Rows: [][]string{ + {"#1", "Add a new | validator", "succeed", "2024-01-01"}, + {"#2", "Change parameter", "timed out", "2024-01-02"}, + }, + }, + expected: `| ID | Title | Status | Date | +| --- | --- | --- | --- | +| #1 | Add a new | validator | succeed | 2024-01-01 | +| #2 | Change parameter | timed out | 2024-01-02 | +`, + }, + { + name: "With Varying Row Sizes", // XXX: should we have a different behavior? + table: mdtable.Table{ + Headers: []string{"ID", "Title"}, + Rows: [][]string{ + {"#1", "Add a new validator"}, + {"#2", "Change parameter", "Extra Column"}, + {"#3", "Fill pool"}, + }, + }, + expected: `| ID | Title | +| --- | --- | +| #1 | Add a new validator | +| #2 | Change parameter | Extra Column | +| #3 | Fill pool | +`, + }, + { + name: "With UTF-8 Characters", + table: mdtable.Table{ + Headers: []string{"ID", "Title", "Status", "Date"}, + Rows: [][]string{ + {"#1", "Café", "succeed", "2024-01-01"}, + {"#2", "München", "timed out", "2024-01-02"}, + {"#3", "São Paulo", "active", "2024-01-03"}, + }, + }, + expected: `| ID | Title | Status | Date | +| --- | --- | --- | --- | +| #1 | Café | succeed | 2024-01-01 | +| #2 | München | timed out | 2024-01-02 | +| #3 | São Paulo | active | 2024-01-03 | +`, + }, + { + name: "With no Headers and no Rows", + table: mdtable.Table{}, + expected: ``, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.table.String() + urequire.Equal(t, got, tt.expected) + }) + } +} + +func TestTableAppend(t *testing.T) { + table := mdtable.Table{ + Headers: []string{"ID", "Title", "Status", "Date"}, + } + + // Use the Append method to add rows to the table + table.Append([]string{"#1", "Add a new validator", "succeed", "2024-01-01"}) + table.Append([]string{"#2", "Change parameter", "timed out", "2024-01-02"}) + table.Append([]string{"#3", "Fill pool", "active", "2024-01-03"}) + got := table.String() + + expected := `| ID | Title | Status | Date | +| --- | --- | --- | --- | +| #1 | Add a new validator | succeed | 2024-01-01 | +| #2 | Change parameter | timed out | 2024-01-02 | +| #3 | Fill pool | active | 2024-01-03 | +` + urequire.Equal(t, got, expected) +} diff --git a/examples/gno.land/r/demo/emit/emit.gno b/examples/gno.land/r/demo/emit/emit.gno new file mode 100644 index 00000000000..a3de8f764a5 --- /dev/null +++ b/examples/gno.land/r/demo/emit/emit.gno @@ -0,0 +1,12 @@ +// Package emit demonstrates how to use the std.Emit() function +// to emit Gno events that can be used to track data changes off-chain. +// std.Emit is variadic; apart from the event name, it can take in any number of key-value pairs to emit. +package emit + +import ( + "std" +) + +func Emit(value string) { + std.Emit("EventName", "key", value) +} diff --git a/examples/gno.land/r/demo/emit/gno.mod b/examples/gno.land/r/demo/emit/gno.mod new file mode 100644 index 00000000000..cf9c2b6b98e --- /dev/null +++ b/examples/gno.land/r/demo/emit/gno.mod @@ -0,0 +1 @@ +module gno.land/r/demo/emit diff --git a/examples/gno.land/r/demo/event/z1_filetest.gno b/examples/gno.land/r/demo/emit/z1_filetest.gno similarity index 61% rename from examples/gno.land/r/demo/event/z1_filetest.gno rename to examples/gno.land/r/demo/emit/z1_filetest.gno index b138aa4351c..7dcdbf8e0a3 100644 --- a/examples/gno.land/r/demo/event/z1_filetest.gno +++ b/examples/gno.land/r/demo/emit/z1_filetest.gno @@ -1,34 +1,34 @@ package main -import "gno.land/r/demo/event" +import "gno.land/r/demo/emit" func main() { - event.Emit("foo") - event.Emit("bar") + emit.Emit("foo") + emit.Emit("bar") } // Events: // [ // { -// "type": "TAG", +// "type": "EventName", // "attrs": [ // { // "key": "key", // "value": "foo" // } // ], -// "pkg_path": "gno.land/r/demo/event", +// "pkg_path": "gno.land/r/demo/emit", // "func": "Emit" // }, // { -// "type": "TAG", +// "type": "EventName", // "attrs": [ // { // "key": "key", // "value": "bar" // } // ], -// "pkg_path": "gno.land/r/demo/event", +// "pkg_path": "gno.land/r/demo/emit", // "func": "Emit" // } // ] diff --git a/examples/gno.land/r/demo/event/event.gno b/examples/gno.land/r/demo/event/event.gno deleted file mode 100644 index 9e5de540734..00000000000 --- a/examples/gno.land/r/demo/event/event.gno +++ /dev/null @@ -1,9 +0,0 @@ -package event - -import ( - "std" -) - -func Emit(value string) { - std.Emit("TAG", "key", value) -} diff --git a/examples/gno.land/r/demo/event/gno.mod b/examples/gno.land/r/demo/event/gno.mod deleted file mode 100644 index 64987d43d79..00000000000 --- a/examples/gno.land/r/demo/event/gno.mod +++ /dev/null @@ -1 +0,0 @@ -module gno.land/r/demo/event diff --git a/examples/gno.land/r/demo/hello_world/gno.mod b/examples/gno.land/r/demo/hello_world/gno.mod new file mode 100644 index 00000000000..9561cd4f077 --- /dev/null +++ b/examples/gno.land/r/demo/hello_world/gno.mod @@ -0,0 +1 @@ +module gno.land/r/demo/hello_world diff --git a/examples/gno.land/r/demo/hello_world/hello.gno b/examples/gno.land/r/demo/hello_world/hello.gno new file mode 100644 index 00000000000..312520de44d --- /dev/null +++ b/examples/gno.land/r/demo/hello_world/hello.gno @@ -0,0 +1,17 @@ +// Package hello_world demonstrates the usage of the Render() function. +// Render() can be called via the vm/qrender ABCI query off-chain to +// retrieve realm state or any other custom data defined by the realm +// developer. The vm/qrender query allows for additional data to be +// passed in with the call, which can be utilized as the path argument +// to the Render() function. This allows developers to create different +// "renders" of their realms depending on the data which is passed in, +// such as pagination, admin dashboards, and more. +package hello_world + +func Render(path string) string { + if path == "" { + return "# Hello, 世界!" + } + + return "# Hello, " + path + "!" +} diff --git a/examples/gno.land/r/demo/hello_world/hello_test.gno b/examples/gno.land/r/demo/hello_world/hello_test.gno new file mode 100644 index 00000000000..4c3d86c556a --- /dev/null +++ b/examples/gno.land/r/demo/hello_world/hello_test.gno @@ -0,0 +1,19 @@ +package hello_world + +import ( + "testing" +) + +func TestHello(t *testing.T) { + expected := "# Hello, 世界!" + got := Render("") + if got != expected { + t.Fatalf("Expected %s, got %s", expected, got) + } + + got = Render("world") + expected = "# Hello, world!" + if got != expected { + t.Fatalf("Expected %s, got %s", expected, got) + } +} diff --git a/examples/gno.land/r/demo/hof/administration.gno b/examples/gno.land/r/demo/hof/administration.gno new file mode 100644 index 00000000000..4b5b212eddf --- /dev/null +++ b/examples/gno.land/r/demo/hof/administration.gno @@ -0,0 +1,24 @@ +package hof + +import "std" + +// Exposing the ownable & pausable APIs +// Should not be needed as soon as MsgCall supports calling methods on exported variables + +func Pause() error { + return exhibition.Pause() +} + +func Unpause() error { + return exhibition.Unpause() +} + +func GetOwner() std.Address { + return owner.Owner() +} + +func TransferOwnership(newOwner std.Address) { + if err := owner.TransferOwnership(newOwner); err != nil { + panic(err) + } +} diff --git a/examples/gno.land/r/demo/hof/errors.gno b/examples/gno.land/r/demo/hof/errors.gno new file mode 100644 index 00000000000..7277f65fa76 --- /dev/null +++ b/examples/gno.land/r/demo/hof/errors.gno @@ -0,0 +1,11 @@ +package hof + +import ( + "errors" +) + +var ( + ErrNoSuchItem = errors.New("hof: no such item exists") + ErrDoubleUpvote = errors.New("hof: cannot upvote twice") + ErrDoubleDownvote = errors.New("hof: cannot downvote twice") +) diff --git a/examples/gno.land/r/demo/hof/gno.mod b/examples/gno.land/r/demo/hof/gno.mod new file mode 100644 index 00000000000..ac5c91295a6 --- /dev/null +++ b/examples/gno.land/r/demo/hof/gno.mod @@ -0,0 +1,15 @@ +module gno.land/r/demo/hof + +require ( + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/avl/pager v0.0.0-latest + gno.land/p/demo/fqname v0.0.0-latest + gno.land/p/demo/ownable v0.0.0-latest + gno.land/p/demo/pausable v0.0.0-latest + gno.land/p/demo/seqid v0.0.0-latest + gno.land/p/demo/testutils v0.0.0-latest + gno.land/p/demo/uassert v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest + gno.land/p/demo/urequire v0.0.0-latest + gno.land/p/moul/txlink v0.0.0-latest +) diff --git a/examples/gno.land/r/demo/hof/hof.gno b/examples/gno.land/r/demo/hof/hof.gno new file mode 100644 index 00000000000..2722c019497 --- /dev/null +++ b/examples/gno.land/r/demo/hof/hof.gno @@ -0,0 +1,132 @@ +// Package hof is the hall of fame realm. +// The Hall of Fame is an exhibition that holds items. Users can add their realms to the Hall of Fame by +// importing the Hall of Fame realm and calling hof.Register() from their init function. +package hof + +import ( + "std" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ownable" + "gno.land/p/demo/pausable" + "gno.land/p/demo/seqid" +) + +var ( + exhibition *Exhibition + owner *ownable.Ownable +) + +type ( + Exhibition struct { + itemCounter seqid.ID + description string + items *avl.Tree // pkgPath > Item + itemsSorted *avl.Tree // same data but sorted, storing pointers + *pausable.Pausable + } + + Item struct { + id seqid.ID + pkgpath string + blockNum int64 + upvote *avl.Tree // std.Addr > struct{}{} + downvote *avl.Tree // std.Addr > struct{}{} + } +) + +func init() { + exhibition = &Exhibition{ + items: avl.NewTree(), + itemsSorted: avl.NewTree(), + } + + owner = ownable.NewWithAddress(std.Address("g125em6arxsnj49vx35f0n0z34putv5ty3376fg5")) + exhibition.Pausable = pausable.NewFromOwnable(owner) +} + +// Register registers your realm to the Hall of Fame +// Should be called from within code +func Register() { + if exhibition.IsPaused() { + return + } + + submission := std.PrevRealm() + pkgpath := submission.PkgPath() + + // Must be called from code + if submission.IsUser() { + return + } + + // Must not yet exist + if exhibition.items.Has(pkgpath) { + return + } + + id := exhibition.itemCounter.Next() + i := &Item{ + id: id, + pkgpath: pkgpath, + blockNum: std.GetHeight(), + upvote: avl.NewTree(), + downvote: avl.NewTree(), + } + + exhibition.items.Set(pkgpath, i) + exhibition.itemsSorted.Set(id.String(), i) + + std.Emit("Registration") +} + +func Upvote(pkgpath string) { + rawItem, ok := exhibition.items.Get(pkgpath) + if !ok { + panic(ErrNoSuchItem.Error()) + } + + item := rawItem.(*Item) + caller := std.PrevRealm().Addr().String() + + if item.upvote.Has(caller) { + panic(ErrDoubleUpvote.Error()) + } + + item.upvote.Set(caller, struct{}{}) +} + +func Downvote(pkgpath string) { + rawItem, ok := exhibition.items.Get(pkgpath) + if !ok { + panic(ErrNoSuchItem.Error()) + } + + item := rawItem.(*Item) + caller := std.PrevRealm().Addr().String() + + if item.downvote.Has(caller) { + panic(ErrDoubleDownvote.Error()) + } + + item.downvote.Set(caller, struct{}{}) +} + +func Delete(pkgpath string) { + if err := owner.CallerIsOwner(); err != nil { + panic(err) + } + + i, ok := exhibition.items.Get(pkgpath) + if !ok { + panic(ErrNoSuchItem.Error()) + } + + if _, removed := exhibition.itemsSorted.Remove(i.(*Item).id.String()); !removed { + panic(ErrNoSuchItem.Error()) + } + + if _, removed := exhibition.items.Remove(pkgpath); !removed { + panic(ErrNoSuchItem.Error()) + } +} diff --git a/examples/gno.land/r/demo/hof/hof_test.gno b/examples/gno.land/r/demo/hof/hof_test.gno new file mode 100644 index 00000000000..72e8d2159be --- /dev/null +++ b/examples/gno.land/r/demo/hof/hof_test.gno @@ -0,0 +1,134 @@ +package hof + +import ( + "std" + "testing" + + "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" +) + +const rlmPath = "gno.land/r/gnoland/home" + +var ( + admin = owner.Owner() + adminRealm = std.NewUserRealm(admin) + alice = testutils.TestAddress("alice") +) + +func TestRegister(t *testing.T) { + // Test user realm register + aliceRealm := std.NewUserRealm(alice) + std.TestSetRealm(aliceRealm) + + Register() + uassert.False(t, itemExists(t, rlmPath)) + + // Test register while paused + std.TestSetRealm(adminRealm) + Pause() + + // Set legitimate caller + std.TestSetRealm(std.NewCodeRealm(rlmPath)) + + Register() + uassert.False(t, itemExists(t, rlmPath)) + + // Unpause + std.TestSetRealm(adminRealm) + Unpause() + + // Set legitimate caller + std.TestSetRealm(std.NewCodeRealm(rlmPath)) + Register() + + // Find registered items + uassert.True(t, itemExists(t, rlmPath)) +} + +func TestUpvote(t *testing.T) { + raw, _ := exhibition.items.Get(rlmPath) + item := raw.(*Item) + + rawSorted, _ := exhibition.itemsSorted.Get(item.id.String()) + itemSorted := rawSorted.(*Item) + + // 0 upvotes by default + urequire.Equal(t, item.upvote.Size(), 0) + + std.TestSetRealm(adminRealm) + + urequire.NotPanics(t, func() { + Upvote(rlmPath) + }) + + // Check both trees for 1 upvote + uassert.Equal(t, item.upvote.Size(), 1) + uassert.Equal(t, itemSorted.upvote.Size(), 1) + + // Check double upvote + uassert.PanicsWithMessage(t, ErrDoubleUpvote.Error(), func() { + Upvote(rlmPath) + }) +} + +func TestDownvote(t *testing.T) { + raw, _ := exhibition.items.Get(rlmPath) + item := raw.(*Item) + + rawSorted, _ := exhibition.itemsSorted.Get(item.id.String()) + itemSorted := rawSorted.(*Item) + + // 0 downvotes by default + urequire.Equal(t, item.downvote.Size(), 0) + + userRealm := std.NewUserRealm(alice) + std.TestSetRealm(userRealm) + + urequire.NotPanics(t, func() { + Downvote(rlmPath) + }) + + // Check both trees for 1 upvote + uassert.Equal(t, item.downvote.Size(), 1) + uassert.Equal(t, itemSorted.downvote.Size(), 1) + + // Check double downvote + uassert.PanicsWithMessage(t, ErrDoubleDownvote.Error(), func() { + Downvote(rlmPath) + }) +} + +func TestDelete(t *testing.T) { + userRealm := std.NewUserRealm(admin) + std.TestSetRealm(userRealm) + std.TestSetOrigCaller(admin) + + uassert.PanicsWithMessage(t, ErrNoSuchItem.Error(), func() { + Delete("nonexistentpkgpath") + }) + + i, _ := exhibition.items.Get(rlmPath) + id := i.(*Item).id + + uassert.NotPanics(t, func() { + Delete(rlmPath) + }) + + uassert.False(t, exhibition.items.Has(rlmPath)) + uassert.False(t, exhibition.itemsSorted.Has(id.String())) +} + +func itemExists(t *testing.T, rlmPath string) bool { + t.Helper() + + i, ok1 := exhibition.items.Get(rlmPath) + ok2 := false + + if ok1 { + _, ok2 = exhibition.itemsSorted.Get(i.(*Item).id.String()) + } + + return ok1 && ok2 +} diff --git a/examples/gno.land/r/demo/hof/render.gno b/examples/gno.land/r/demo/hof/render.gno new file mode 100644 index 00000000000..6b06ef04051 --- /dev/null +++ b/examples/gno.land/r/demo/hof/render.gno @@ -0,0 +1,113 @@ +package hof + +import ( + "strings" + + "gno.land/p/demo/avl/pager" + "gno.land/p/demo/fqname" + "gno.land/p/demo/seqid" + "gno.land/p/demo/ufmt" + "gno.land/p/moul/txlink" +) + +const ( + pageSize = 5 +) + +func Render(path string) string { + out := "# Hall of Fame\n\n" + + dashboardEnabled := path == "dashboard" + + if dashboardEnabled { + out += renderDashboard() + } + + out += exhibition.Render(path, dashboardEnabled) + + return out +} + +func (e Exhibition) Render(path string, dashboard bool) string { + out := ufmt.Sprintf("%s\n\n", e.description) + + if e.items.Size() == 0 { + out += "No items in this exhibition currently.\n\n" + return out + } + + out += "
\n\n" + + page := pager.NewPager(e.itemsSorted, pageSize).MustGetPageByPath(path) + + for i := len(page.Items) - 1; i >= 0; i-- { + item := page.Items[i] + + out += "
\n\n" + id, _ := seqid.FromString(item.Key) + out += ufmt.Sprintf("### Submission #%d\n\n", int(id)) + out += item.Value.(*Item).Render(dashboard) + out += "
" + } + + out += "
\n\n" + + out += page.Selector() + + return out +} + +func (i Item) Render(dashboard bool) string { + out := ufmt.Sprintf("\n```\n%s\n```\n\n", i.pkgpath) + out += ufmt.Sprintf("by %s\n\n", strings.Split(i.pkgpath, "/")[2]) + out += ufmt.Sprintf("[View realm](%s)\n\n", strings.TrimPrefix(i.pkgpath, "gno.land")) // gno.land/r/leon/home > /r/leon/home + out += ufmt.Sprintf("Submitted at Block #%d\n\n", i.blockNum) + + out += ufmt.Sprintf("#### [%d👍](%s) - [%d👎](%s)\n\n", + i.upvote.Size(), txlink.URL("Upvote", "pkgpath", i.pkgpath), + i.downvote.Size(), txlink.URL("Downvote", "pkgpath", i.pkgpath), + ) + + if dashboard { + out += ufmt.Sprintf("[Delete](%s)", txlink.URL("Delete", "pkgpath", i.pkgpath)) + } + + return out +} + +func renderDashboard() string { + out := "---\n\n" + out += "## Dashboard\n\n" + out += ufmt.Sprintf("Total submissions: %d\n\n", exhibition.items.Size()) + + out += ufmt.Sprintf("Exhibition admin: %s\n\n", owner.Owner().String()) + + if !exhibition.IsPaused() { + out += ufmt.Sprintf("[Pause exhibition](%s)\n\n", txlink.URL("Pause")) + } else { + out += ufmt.Sprintf("[Unpause exhibition](%s)\n\n", txlink.URL("Unpause")) + } + + out += "---\n\n" + + return out +} + +func RenderExhibWidget(itemsToRender int) string { + if itemsToRender < 1 { + return "" + } + + out := "" + i := 0 + exhibition.items.Iterate("", "", func(key string, value interface{}) bool { + item := value.(*Item) + + out += ufmt.Sprintf("- %s\n", fqname.RenderLink(item.pkgpath, "")) + + i++ + return i >= itemsToRender + }) + + return out +} diff --git a/examples/gno.land/r/gnoland/home/gno.mod b/examples/gno.land/r/gnoland/home/gno.mod index c208ad421c9..ff52ef4c8b1 100644 --- a/examples/gno.land/r/gnoland/home/gno.mod +++ b/examples/gno.land/r/gnoland/home/gno.mod @@ -4,6 +4,7 @@ require ( gno.land/p/demo/ownable v0.0.0-latest gno.land/p/demo/ufmt v0.0.0-latest gno.land/p/demo/ui v0.0.0-latest + gno.land/r/demo/hof v0.0.0-latest gno.land/r/gnoland/blog v0.0.0-latest gno.land/r/gnoland/events v0.0.0-latest ) diff --git a/examples/gno.land/r/gnoland/home/home.gno b/examples/gno.land/r/gnoland/home/home.gno index c6b3929a16c..ce976923ef5 100644 --- a/examples/gno.land/r/gnoland/home/home.gno +++ b/examples/gno.land/r/gnoland/home/home.gno @@ -6,6 +6,7 @@ import ( "gno.land/p/demo/ownable" "gno.land/p/demo/ufmt" "gno.land/p/demo/ui" + "gno.land/r/demo/hof" blog "gno.land/r/gnoland/blog" events "gno.land/r/gnoland/events" ) @@ -37,7 +38,7 @@ func Render(_ string) string { ui.Columns{3, []ui.Element{ lastBlogposts(4), upcomingEvents(), - lastContributions(4), + latestHOFItems(5), }}, ) @@ -90,6 +91,15 @@ func upcomingEvents() ui.Element { } } +func latestHOFItems(num int) ui.Element { + submissions := hof.RenderExhibWidget(num) + + return ui.Element{ + ui.H3("[Hall of Fame](/r/demo/hof)"), + ui.Text(submissions), + } +} + func introSection() ui.Element { return ui.Element{ ui.H3("We’re building gno.land, set to become the leading open-source smart contract platform, using Gno, an interpreted and fully deterministic variation of the Go programming language for succinct and composable smart contracts."), @@ -270,7 +280,7 @@ func discoverLinks() ui.Element { - [Gnoscan](https://gnoscan.io) - [Portal Loop](https://docs.gno.land/concepts/portal-loop) - [Testnet 4](https://test4.gno.land/) -- Testnet Faucet Hub (soon) +- [Faucet Hub](https://faucet.gno.land) `), diff --git a/examples/gno.land/r/gnoland/home/home_filetest.gno b/examples/gno.land/r/gnoland/home/home_filetest.gno index b22c22567b3..c587af9b817 100644 --- a/examples/gno.land/r/gnoland/home/home_filetest.gno +++ b/examples/gno.land/r/gnoland/home/home_filetest.gno @@ -57,7 +57,7 @@ func main() { // - [Gnoscan](https://gnoscan.io) // - [Portal Loop](https://docs.gno.land/concepts/portal-loop) // - [Testnet 4](https://test4.gno.land/) -// - Testnet Faucet Hub (soon) +// - [Faucet Hub](https://faucet.gno.land) // // // @@ -78,9 +78,9 @@ func main() { // //
// -// ### Latest Contributions +// ### [Hall of Fame](/r/demo/hof) +// // -// [View latest contributions](https://github.com/gnolang/gno/pulls) //
// // diff --git a/examples/gno.land/r/leon/home/gno.mod b/examples/gno.land/r/leon/home/gno.mod index 48cf64a9d0a..4649cf4abe6 100644 --- a/examples/gno.land/r/leon/home/gno.mod +++ b/examples/gno.land/r/leon/home/gno.mod @@ -4,5 +4,6 @@ require ( gno.land/p/demo/ufmt v0.0.0-latest gno.land/r/demo/art/gnoface v0.0.0-latest gno.land/r/demo/art/millipede v0.0.0-latest + gno.land/r/demo/hof v0.0.0-latest gno.land/r/leon/config v0.0.0-latest ) diff --git a/examples/gno.land/r/leon/home/home.gno b/examples/gno.land/r/leon/home/home.gno index ba688792a4c..aea8b43e9cd 100644 --- a/examples/gno.land/r/leon/home/home.gno +++ b/examples/gno.land/r/leon/home/home.gno @@ -8,6 +8,7 @@ import ( "gno.land/r/demo/art/gnoface" "gno.land/r/demo/art/millipede" + "gno.land/r/demo/hof" "gno.land/r/leon/config" ) @@ -31,6 +32,8 @@ My contributions to gno.land can mainly be found TODO import r/gh `, } + + hof.Register() } func UpdatePFP(url, caption string) { diff --git a/examples/gno.land/r/manfred/home/gno.mod b/examples/gno.land/r/manfred/home/gno.mod index 6e7aac70cc7..9885cac19c2 100644 --- a/examples/gno.land/r/manfred/home/gno.mod +++ b/examples/gno.land/r/manfred/home/gno.mod @@ -1,3 +1,6 @@ module gno.land/r/manfred/home -require gno.land/r/manfred/config v0.0.0-latest +require ( + gno.land/r/demo/hof v0.0.0-latest + gno.land/r/manfred/config v0.0.0-latest +) diff --git a/examples/gno.land/r/manfred/home/home.gno b/examples/gno.land/r/manfred/home/home.gno index 720796a2201..4766f54e51f 100644 --- a/examples/gno.land/r/manfred/home/home.gno +++ b/examples/gno.land/r/manfred/home/home.gno @@ -1,6 +1,9 @@ package home -import "gno.land/r/manfred/config" +import ( + "gno.land/r/demo/hof" + "gno.land/r/manfred/config" +) var ( todos []string @@ -12,6 +15,7 @@ func init() { todos = append(todos, "fill this todo list...") status = "Online" // Initial status set to "Online" memeImgURL = "https://i.imgflip.com/7ze8dc.jpg" + hof.Register() } func Render(path string) string { diff --git a/examples/gno.land/r/morgan/home/gno.mod b/examples/gno.land/r/morgan/home/gno.mod index 573a7e139e7..35e2fbb2119 100644 --- a/examples/gno.land/r/morgan/home/gno.mod +++ b/examples/gno.land/r/morgan/home/gno.mod @@ -1 +1,3 @@ module gno.land/r/morgan/home + +require gno.land/r/demo/hof v0.0.0-latest diff --git a/examples/gno.land/r/morgan/home/home.gno b/examples/gno.land/r/morgan/home/home.gno index 33d7e0b2df7..571f14ed5ec 100644 --- a/examples/gno.land/r/morgan/home/home.gno +++ b/examples/gno.land/r/morgan/home/home.gno @@ -1,10 +1,14 @@ package home +import "gno.land/r/demo/hof" + const staticHome = `# morgan's (gn)home - [📝 sign my guestbook](/r/morgan/guestbook) ` +func init() { hof.Register() } + func Render(path string) string { return staticHome } diff --git a/gnovm/pkg/gnolang/nodes.go b/gnovm/pkg/gnolang/nodes.go index c282b619fdc..45062f8e14c 100644 --- a/gnovm/pkg/gnolang/nodes.go +++ b/gnovm/pkg/gnolang/nodes.go @@ -149,16 +149,17 @@ func (loc Location) IsZero() bool { type GnoAttribute string const ( - ATTR_PREPROCESSED GnoAttribute = "ATTR_PREPROCESSED" - ATTR_PREDEFINED GnoAttribute = "ATTR_PREDEFINED" - ATTR_TYPE_VALUE GnoAttribute = "ATTR_TYPE_VALUE" - ATTR_TYPEOF_VALUE GnoAttribute = "ATTR_TYPEOF_VALUE" - ATTR_IOTA GnoAttribute = "ATTR_IOTA" - ATTR_LOCATIONED GnoAttribute = "ATTR_LOCATIONE" // XXX DELETE - ATTR_GOTOLOOP_STMT GnoAttribute = "ATTR_GOTOLOOP_STMT" // XXX delete? - ATTR_LOOP_DEFINES GnoAttribute = "ATTR_LOOP_DEFINES" // []Name defined within loops. - ATTR_LOOP_USES GnoAttribute = "ATTR_LOOP_USES" // []Name loop defines actually used. - ATTR_SHIFT_RHS GnoAttribute = "ATTR_SHIFT_RHS" + ATTR_PREPROCESSED GnoAttribute = "ATTR_PREPROCESSED" + ATTR_PREDEFINED GnoAttribute = "ATTR_PREDEFINED" + ATTR_TYPE_VALUE GnoAttribute = "ATTR_TYPE_VALUE" + ATTR_TYPEOF_VALUE GnoAttribute = "ATTR_TYPEOF_VALUE" + ATTR_IOTA GnoAttribute = "ATTR_IOTA" + ATTR_LOCATIONED GnoAttribute = "ATTR_LOCATIONE" // XXX DELETE + ATTR_GOTOLOOP_STMT GnoAttribute = "ATTR_GOTOLOOP_STMT" // XXX delete? + ATTR_LOOP_DEFINES GnoAttribute = "ATTR_LOOP_DEFINES" // []Name defined within loops. + 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" ) type Attributes struct { diff --git a/gnovm/pkg/gnolang/op_exec.go b/gnovm/pkg/gnolang/op_exec.go index a61349b0806..900b5f8e9bb 100644 --- a/gnovm/pkg/gnolang/op_exec.go +++ b/gnovm/pkg/gnolang/op_exec.go @@ -676,25 +676,15 @@ EXEC_SWITCH: bs.Active = bs.Body[cs.BodyIndex] // prefill case FALLTHROUGH: ss, ok := m.LastFrame().Source.(*SwitchStmt) + // this is handled in the preprocessor + // should never happen if !ok { - // fallthrough is only allowed in a switch statement panic("fallthrough statement out of place") } - if ss.IsTypeSwitch { - // fallthrough is not allowed in type switches - panic("cannot fallthrough in type switch") - } + b := m.LastBlock() - if b.bodyStmt.NextBodyIndex != len(b.bodyStmt.Body) { - // fallthrough is not the final statement - panic("fallthrough statement out of place") - } // compute next switch clause from BodyIndex (assigned in preprocess) nextClause := cs.BodyIndex + 1 - if nextClause >= len(ss.Clauses) { - // no more clause after the one executed, this is not allowed - panic("cannot fallthrough final case in switch") - } // expand block size cl := ss.Clauses[nextClause] if nn := cl.GetNumNames(); int(nn) > len(b.Values) { diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index 59de1fd37ce..b7c22e0b9f6 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -298,6 +298,11 @@ func initStaticBlocks(store Store, ctx BlockNode, bn BlockNode) { last.Predefine(false, n.VarName) } case *SwitchClauseStmt: + blen := len(n.Body) + if blen > 0 { + n.Body[blen-1].SetAttribute(ATTR_LAST_BLOCK_STMT, true) + } + // parent switch statement. ss := ns[len(ns)-1].(*SwitchStmt) // anything declared in ss are copied, @@ -2089,9 +2094,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { switch n.Op { case BREAK: if n.Label == "" { - if !findBreakableNode(ns) { - panic("cannot break with no parent loop or switch") - } + findBreakableNode(last, store) } else { // Make sure that the label exists, either for a switch or a // BranchStmt. @@ -2101,9 +2104,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { } case CONTINUE: if n.Label == "" { - if !findContinuableNode(ns) { - panic("cannot continue with no parent loop") - } + findContinuableNode(last, store) } else { if isSwitchLabel(ns, n.Label) { panic(fmt.Sprintf("invalid continue label %q\n", n.Label)) @@ -2115,17 +2116,36 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { n.Depth = depth n.BodyIndex = index case FALLTHROUGH: - if swchC, ok := last.(*SwitchClauseStmt); ok { - // last is a switch clause, find its index in the switch and assign - // it to the fallthrough node BodyIndex. This will be used at - // runtime to determine the next switch clause to run. - swch := lastSwitch(ns) - for i := range swch.Clauses { - if &swch.Clauses[i] == swchC { - // switch clause found - n.BodyIndex = i - break - } + swchC, ok := last.(*SwitchClauseStmt) + if !ok { + // fallthrough is only allowed in a switch statement + panic("fallthrough statement out of place") + } + + if n.GetAttribute(ATTR_LAST_BLOCK_STMT) != true { + // no more clause after the one executed, this is not allowed + panic("fallthrough statement out of place") + } + + // last is a switch clause, find its index in the switch and assign + // it to the fallthrough node BodyIndex. This will be used at + // runtime to determine the next switch clause to run. + swch := lastSwitch(ns) + + if swch.IsTypeSwitch { + // fallthrough is not allowed in type switches + panic("cannot fallthrough in type switch") + } + + for i := range swch.Clauses { + if i == len(swch.Clauses)-1 { + panic("cannot fallthrough final case in switch") + } + + if &swch.Clauses[i] == swchC { + // switch clause found + n.BodyIndex = i + break } } default: @@ -3295,24 +3315,36 @@ func funcOf(last BlockNode) (BlockNode, *FuncTypeExpr) { } } -func findBreakableNode(ns []Node) bool { - for _, n := range ns { - switch n.(type) { - case *ForStmt, *RangeStmt, *SwitchClauseStmt: - return true +func findBreakableNode(last BlockNode, store Store) { + for last != nil { + switch last.(type) { + case *FuncLitExpr, *FuncDecl: + panic("break statement out of place") + case *ForStmt: + return + case *RangeStmt: + return + case *SwitchClauseStmt: + return } + + last = last.GetParentNode(store) } - return false } -func findContinuableNode(ns []Node) bool { - for _, n := range ns { - switch n.(type) { - case *ForStmt, *RangeStmt: - return true +func findContinuableNode(last BlockNode, store Store) { + for last != nil { + switch last.(type) { + case *FuncLitExpr, *FuncDecl: + panic("continue statement out of place") + case *ForStmt: + return + case *RangeStmt: + return } + + last = last.GetParentNode(store) } - return false } func findBranchLabel(last BlockNode, label Name) ( diff --git a/gnovm/tests/files/break0.gno b/gnovm/tests/files/break0.gno index 17d68dc1dbf..891084c56f9 100644 --- a/gnovm/tests/files/break0.gno +++ b/gnovm/tests/files/break0.gno @@ -5,4 +5,4 @@ func main() { } // Error: -// main/files/break0.gno:4:2: cannot break with no parent loop or switch +// main/files/break0.gno:4:2: break statement out of place diff --git a/gnovm/tests/files/cont3.gno b/gnovm/tests/files/cont3.gno index 8a305d4ceb2..39112697860 100644 --- a/gnovm/tests/files/cont3.gno +++ b/gnovm/tests/files/cont3.gno @@ -5,4 +5,4 @@ func main() { } // Error: -// main/files/cont3.gno:4:2: cannot continue with no parent loop +// main/files/cont3.gno:4:2: continue statement out of place diff --git a/gnovm/tests/files/for21.gno b/gnovm/tests/files/for21.gno new file mode 100644 index 00000000000..74b7d724121 --- /dev/null +++ b/gnovm/tests/files/for21.gno @@ -0,0 +1,17 @@ +package main + +func main() { + for i := 0; i < 10; i++ { + if i == 1 { + _ = func() int { + continue + return 11 + }() + } + println(i) + } + println("wat???") +} + +// Error: +// main/files/for21.gno:7:17: continue statement out of place diff --git a/gnovm/tests/files/for22.gno b/gnovm/tests/files/for22.gno new file mode 100644 index 00000000000..dd86ce97cb7 --- /dev/null +++ b/gnovm/tests/files/for22.gno @@ -0,0 +1,17 @@ +package main + +func main() { + for i := 0; i < 10; i++ { + if i == 1 { + _ = func() int { + fallthrough + return 11 + }() + } + println(i) + } + println("wat???") +} + +// Error: +// main/files/for22.gno:7:17: fallthrough statement out of place \ No newline at end of file diff --git a/gnovm/tests/files/for23.gno b/gnovm/tests/files/for23.gno new file mode 100644 index 00000000000..cb0f4c104fa --- /dev/null +++ b/gnovm/tests/files/for23.gno @@ -0,0 +1,17 @@ +package main + +func main() { + for i := 0; i < 10; i++ { + if i == 1 { + _ = func() int { + break + return 11 + }() + } + println(i) + } + println("wat???") +} + +// Error: +// main/files/for23.gno:7:17: break statement out of place \ No newline at end of file diff --git a/gnovm/tests/files/for24.gno b/gnovm/tests/files/for24.gno new file mode 100644 index 00000000000..c3d49bb86a7 --- /dev/null +++ b/gnovm/tests/files/for24.gno @@ -0,0 +1,19 @@ +package main + +func main() { + for i := 0; i < 10; i++ { + if i == 1 { + _ = func() int { + if true { + break + } + return 11 + }() + } + println(i) + } + println("wat???") +} + +// Error: +// main/files/for24.gno:8:21: break statement out of place \ No newline at end of file diff --git a/gnovm/tests/files/switch8.gno b/gnovm/tests/files/switch8.gno index c43c72582c0..f5952354270 100644 --- a/gnovm/tests/files/switch8.gno +++ b/gnovm/tests/files/switch8.gno @@ -7,4 +7,4 @@ func main() { } // Error: -// fallthrough statement out of place +// main/files/switch8.gno:5:2: fallthrough statement out of place diff --git a/gnovm/tests/files/switch8b.gno b/gnovm/tests/files/switch8b.gno index cdf35caf784..079b1b48efe 100644 --- a/gnovm/tests/files/switch8b.gno +++ b/gnovm/tests/files/switch8b.gno @@ -12,4 +12,4 @@ func main() { } // Error: -// cannot fallthrough final case in switch +// main/files/switch8b.gno:10:3: cannot fallthrough final case in switch diff --git a/gnovm/tests/files/switch8c.gno b/gnovm/tests/files/switch8c.gno index 6897b8a88fd..27c8e3ad9d5 100644 --- a/gnovm/tests/files/switch8c.gno +++ b/gnovm/tests/files/switch8c.gno @@ -12,4 +12,4 @@ func main() { } // Error: -// fallthrough statement out of place +// main/files/switch8c.gno:7:3: fallthrough statement out of place diff --git a/gnovm/tests/files/switch9.gno b/gnovm/tests/files/switch9.gno index 5f596de013a..5b05316b0b9 100644 --- a/gnovm/tests/files/switch9.gno +++ b/gnovm/tests/files/switch9.gno @@ -13,4 +13,4 @@ func main() { } // Error: -// cannot fallthrough in type switch +// main/files/switch9.gno:9:3: cannot fallthrough in type switch