diff --git a/contribs/gnomigrate/internal/txs/txs.go b/contribs/gnomigrate/internal/txs/txs.go
index 4c65ca6ef0b..231428d5064 100644
--- a/contribs/gnomigrate/internal/txs/txs.go
+++ b/contribs/gnomigrate/internal/txs/txs.go
@@ -184,7 +184,7 @@ func processFile(ctx context.Context, io commands.IO, source, destination string
continue
}
- if _, err = outputFile.WriteString(fmt.Sprintf("%s\n", string(marshaledData))); err != nil {
+ if _, err = fmt.Fprintf(outputFile, "%s\n", marshaledData); err != nil {
io.ErrPrintfln("unable to save to output file, %s", err)
}
}
diff --git a/examples/gno.land/p/moul/addrset/addrset.gno b/examples/gno.land/p/moul/addrset/addrset.gno
new file mode 100644
index 00000000000..0bb8165f9fe
--- /dev/null
+++ b/examples/gno.land/p/moul/addrset/addrset.gno
@@ -0,0 +1,100 @@
+// Package addrset provides a specialized set data structure for managing unique Gno addresses.
+//
+// It is built on top of an AVL tree for efficient operations and maintains addresses in sorted order.
+// This package is particularly useful when you need to:
+// - Track a collection of unique addresses (e.g., for whitelists, participants, etc.)
+// - Efficiently check address membership
+// - Support pagination when displaying addresses
+//
+// Example usage:
+//
+// import (
+// "std"
+// "gno.land/p/moul/addrset"
+// )
+//
+// func MyHandler() {
+// // Create a new address set
+// var set addrset.Set
+//
+// // Add some addresses
+// addr1 := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")
+// addr2 := std.Address("g1sss5g0rkqr88k4u648yd5d3l9t4d8vvqwszqth")
+//
+// set.Add(addr1) // returns true (newly added)
+// set.Add(addr2) // returns true (newly added)
+// set.Add(addr1) // returns false (already exists)
+//
+// // Check membership
+// if set.Has(addr1) {
+// // addr1 is in the set
+// }
+//
+// // Get size
+// size := set.Size() // returns 2
+//
+// // Iterate with pagination (10 items per page, starting at offset 0)
+// set.IterateByOffset(0, 10, func(addr std.Address) bool {
+// // Process addr
+// return false // continue iteration
+// })
+//
+// // Remove an address
+// set.Remove(addr1) // returns true (was present)
+// set.Remove(addr1) // returns false (not present)
+// }
+package addrset
+
+import (
+ "std"
+
+ "gno.land/p/demo/avl"
+)
+
+type Set struct {
+ tree avl.Tree
+}
+
+// Add inserts an address into the set.
+// Returns true if the address was newly added, false if it already existed.
+func (s *Set) Add(addr std.Address) bool {
+ return !s.tree.Set(string(addr), nil)
+}
+
+// Remove deletes an address from the set.
+// Returns true if the address was found and removed, false if it didn't exist.
+func (s *Set) Remove(addr std.Address) bool {
+ _, removed := s.tree.Remove(string(addr))
+ return removed
+}
+
+// Has checks if an address exists in the set.
+func (s *Set) Has(addr std.Address) bool {
+ return s.tree.Has(string(addr))
+}
+
+// Size returns the number of addresses in the set.
+func (s *Set) Size() int {
+ return s.tree.Size()
+}
+
+// IterateByOffset walks through addresses starting at the given offset.
+// The callback should return true to stop iteration.
+func (s *Set) IterateByOffset(offset int, count int, cb func(addr std.Address) bool) {
+ s.tree.IterateByOffset(offset, count, func(key string, _ interface{}) bool {
+ return cb(std.Address(key))
+ })
+}
+
+// ReverseIterateByOffset walks through addresses in reverse order starting at the given offset.
+// The callback should return true to stop iteration.
+func (s *Set) ReverseIterateByOffset(offset int, count int, cb func(addr std.Address) bool) {
+ s.tree.ReverseIterateByOffset(offset, count, func(key string, _ interface{}) bool {
+ return cb(std.Address(key))
+ })
+}
+
+// Tree returns the underlying AVL tree for advanced usage.
+func (s *Set) Tree() avl.ITree {
+ return &s.tree
+}
diff --git a/examples/gno.land/p/moul/addrset/addrset_test.gno b/examples/gno.land/p/moul/addrset/addrset_test.gno
new file mode 100644
index 00000000000..c3e27eab1df
--- /dev/null
+++ b/examples/gno.land/p/moul/addrset/addrset_test.gno
@@ -0,0 +1,174 @@
+package addrset
+
+import (
+ "std"
+ "testing"
+
+ "gno.land/p/demo/uassert"
+)
+
+func TestSet(t *testing.T) {
+ addr1 := std.Address("addr1")
+ addr2 := std.Address("addr2")
+ addr3 := std.Address("addr3")
+
+ tests := []struct {
+ name string
+ actions func(s *Set)
+ size int
+ has map[std.Address]bool
+ addrs []std.Address // for iteration checks
+ }{
+ {
+ name: "empty set",
+ actions: func(s *Set) {},
+ size: 0,
+ has: map[std.Address]bool{addr1: false},
+ },
+ {
+ name: "single address",
+ actions: func(s *Set) {
+ s.Add(addr1)
+ },
+ size: 1,
+ has: map[std.Address]bool{
+ addr1: true,
+ addr2: false,
+ },
+ addrs: []std.Address{addr1},
+ },
+ {
+ name: "multiple addresses",
+ actions: func(s *Set) {
+ s.Add(addr1)
+ s.Add(addr2)
+ s.Add(addr3)
+ },
+ size: 3,
+ has: map[std.Address]bool{
+ addr1: true,
+ addr2: true,
+ addr3: true,
+ },
+ addrs: []std.Address{addr1, addr2, addr3},
+ },
+ {
+ name: "remove address",
+ actions: func(s *Set) {
+ s.Add(addr1)
+ s.Add(addr2)
+ s.Remove(addr1)
+ },
+ size: 1,
+ has: map[std.Address]bool{
+ addr1: false,
+ addr2: true,
+ },
+ addrs: []std.Address{addr2},
+ },
+ {
+ name: "duplicate adds",
+ actions: func(s *Set) {
+ uassert.True(t, s.Add(addr1)) // first add returns true
+ uassert.False(t, s.Add(addr1)) // second add returns false
+ uassert.True(t, s.Remove(addr1)) // remove existing returns true
+ uassert.False(t, s.Remove(addr1)) // remove non-existing returns false
+ },
+ size: 0,
+ has: map[std.Address]bool{
+ addr1: false,
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ var set Set
+
+ // Execute test actions
+ tt.actions(&set)
+
+ // Check size
+ uassert.Equal(t, tt.size, set.Size())
+
+ // Check existence
+ for addr, expected := range tt.has {
+ uassert.Equal(t, expected, set.Has(addr))
+ }
+
+ // Check iteration if addresses are specified
+ if tt.addrs != nil {
+ collected := []std.Address{}
+ set.IterateByOffset(0, 10, func(addr std.Address) bool {
+ collected = append(collected, addr)
+ return false
+ })
+
+ // Check length
+ uassert.Equal(t, len(tt.addrs), len(collected))
+
+ // Check each address
+ for i, addr := range tt.addrs {
+ uassert.Equal(t, addr, collected[i])
+ }
+ }
+ })
+ }
+}
+
+func TestSetIterationLimits(t *testing.T) {
+ tests := []struct {
+ name string
+ addrs []std.Address
+ offset int
+ limit int
+ expected int
+ }{
+ {
+ name: "zero offset full list",
+ addrs: []std.Address{"a1", "a2", "a3"},
+ offset: 0,
+ limit: 10,
+ expected: 3,
+ },
+ {
+ name: "offset with limit",
+ addrs: []std.Address{"a1", "a2", "a3", "a4"},
+ offset: 1,
+ limit: 2,
+ expected: 2,
+ },
+ {
+ name: "offset beyond size",
+ addrs: []std.Address{"a1", "a2"},
+ offset: 3,
+ limit: 1,
+ expected: 0,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ var set Set
+ for _, addr := range tt.addrs {
+ set.Add(addr)
+ }
+
+ // Test forward iteration
+ count := 0
+ set.IterateByOffset(tt.offset, tt.limit, func(addr std.Address) bool {
+ count++
+ return false
+ })
+ uassert.Equal(t, tt.expected, count)
+
+ // Test reverse iteration
+ count = 0
+ set.ReverseIterateByOffset(tt.offset, tt.limit, func(addr std.Address) bool {
+ count++
+ return false
+ })
+ uassert.Equal(t, tt.expected, count)
+ })
+ }
+}
diff --git a/examples/gno.land/p/moul/addrset/gno.mod b/examples/gno.land/p/moul/addrset/gno.mod
new file mode 100644
index 00000000000..45bb53b399c
--- /dev/null
+++ b/examples/gno.land/p/moul/addrset/gno.mod
@@ -0,0 +1 @@
+module gno.land/p/moul/addrset
diff --git a/examples/gno.land/r/demo/btree_dao/btree_dao.gno b/examples/gno.land/r/demo/btree_dao/btree_dao.gno
new file mode 100644
index 00000000000..c90742eb29b
--- /dev/null
+++ b/examples/gno.land/r/demo/btree_dao/btree_dao.gno
@@ -0,0 +1,209 @@
+package btree_dao
+
+import (
+ "errors"
+ "std"
+ "strings"
+ "time"
+
+ "gno.land/p/demo/btree"
+ "gno.land/p/demo/grc/grc721"
+ "gno.land/p/demo/ufmt"
+ "gno.land/p/moul/md"
+)
+
+// RegistrationDetails holds the details of a user's registration in the BTree DAO.
+// It stores the user's address, registration time, their B-Tree if they planted one,
+// and their NFT ID.
+type RegistrationDetails struct {
+ Address std.Address
+ RegTime time.Time
+ UserBTree *btree.BTree
+ NFTID string
+}
+
+// Less implements the btree.Record interface for RegistrationDetails.
+// It compares two RegistrationDetails based on their registration time.
+// Returns true if the current registration time is before the other registration time.
+func (rd *RegistrationDetails) Less(than btree.Record) bool {
+ other := than.(*RegistrationDetails)
+ return rd.RegTime.Before(other.RegTime)
+}
+
+var (
+ dao = grc721.NewBasicNFT("BTree DAO", "BTDAO")
+ tokenID = 0
+ members = btree.New()
+)
+
+// PlantTree allows a user to plant their B-Tree in the DAO forest.
+// It mints an NFT to the user and registers their tree in the DAO.
+// Returns an error if the tree is already planted, empty, or if NFT minting fails.
+func PlantTree(userBTree *btree.BTree) error {
+ return plantImpl(userBTree, "")
+}
+
+// PlantSeed allows a user to register as a seed in the DAO with a message.
+// It mints an NFT to the user and registers them as a seed member.
+// Returns an error if the message is empty or if NFT minting fails.
+func PlantSeed(message string) error {
+ return plantImpl(nil, message)
+}
+
+// plantImpl is the internal implementation that handles both tree planting and seed registration.
+// For tree planting (userBTree != nil), it verifies the tree isn't already planted and isn't empty.
+// For seed planting (userBTree == nil), it verifies the seed message isn't empty.
+// In both cases, it mints an NFT to the user and adds their registration details to the members tree.
+// Returns an error if any validation fails or if NFT minting fails.
+func plantImpl(userBTree *btree.BTree, seedMessage string) error {
+ // Get the caller's address
+ userAddress := std.GetOrigCaller()
+
+ var nftID string
+ var regDetails *RegistrationDetails
+
+ if userBTree != nil {
+ // Handle tree planting
+ var treeExists bool
+ members.Ascend(func(record btree.Record) bool {
+ regDetails := record.(*RegistrationDetails)
+ if regDetails.UserBTree == userBTree {
+ treeExists = true
+ return false
+ }
+ return true
+ })
+ if treeExists {
+ return errors.New("tree is already planted in the forest")
+ }
+
+ if userBTree.Len() == 0 {
+ return errors.New("cannot plant an empty tree")
+ }
+
+ nftID = ufmt.Sprintf("%d", tokenID)
+ regDetails = &RegistrationDetails{
+ Address: userAddress,
+ RegTime: time.Now(),
+ UserBTree: userBTree,
+ NFTID: nftID,
+ }
+ } else {
+ // Handle seed planting
+ if seedMessage == "" {
+ return errors.New("seed message cannot be empty")
+ }
+ nftID = "seed_" + ufmt.Sprintf("%d", tokenID)
+ regDetails = &RegistrationDetails{
+ Address: userAddress,
+ RegTime: time.Now(),
+ UserBTree: nil,
+ NFTID: nftID,
+ }
+ }
+
+ // Mint an NFT to the user
+ err := dao.Mint(userAddress, grc721.TokenID(nftID))
+ if err != nil {
+ return err
+ }
+
+ members.Insert(regDetails)
+ tokenID++
+ return nil
+}
+
+// Render generates a Markdown representation of the DAO members.
+// It displays:
+// - Total number of NFTs minted
+// - Total number of members
+// - Size of the biggest planted tree
+// - The first 3 members (OGs)
+// - The latest 10 members
+// Each member entry includes their address and owned NFTs (🌳 for trees, 🌱 for seeds).
+// The path parameter is currently unused.
+// Returns a formatted Markdown string.
+func Render(path string) string {
+ var latestMembers []string
+ var ogMembers []string
+
+ // Get total size and first member
+ totalSize := members.Len()
+ biggestTree := 0
+ if maxMember := members.Max(); maxMember != nil {
+ if userBTree := maxMember.(*RegistrationDetails).UserBTree; userBTree != nil {
+ biggestTree = userBTree.Len()
+ }
+ }
+
+ // Collect the latest 10 members
+ members.Descend(func(record btree.Record) bool {
+ if len(latestMembers) < 10 {
+ regDetails := record.(*RegistrationDetails)
+ addr := regDetails.Address
+ nftList := ""
+ balance, err := dao.BalanceOf(addr)
+ if err == nil && balance > 0 {
+ nftList = " (NFTs: "
+ for i := uint64(0); i < balance; i++ {
+ if i > 0 {
+ nftList += ", "
+ }
+ if regDetails.UserBTree == nil {
+ nftList += "🌱#" + regDetails.NFTID
+ } else {
+ nftList += "🌳#" + regDetails.NFTID
+ }
+ }
+ nftList += ")"
+ }
+ latestMembers = append(latestMembers, string(addr)+nftList)
+ return true
+ }
+ return false
+ })
+
+ // Collect the first 3 members (OGs)
+ members.Ascend(func(record btree.Record) bool {
+ if len(ogMembers) < 3 {
+ regDetails := record.(*RegistrationDetails)
+ addr := regDetails.Address
+ nftList := ""
+ balance, err := dao.BalanceOf(addr)
+ if err == nil && balance > 0 {
+ nftList = " (NFTs: "
+ for i := uint64(0); i < balance; i++ {
+ if i > 0 {
+ nftList += ", "
+ }
+ if regDetails.UserBTree == nil {
+ nftList += "🌱#" + regDetails.NFTID
+ } else {
+ nftList += "🌳#" + regDetails.NFTID
+ }
+ }
+ nftList += ")"
+ }
+ ogMembers = append(ogMembers, string(addr)+nftList)
+ return true
+ }
+ return false
+ })
+
+ var sb strings.Builder
+
+ sb.WriteString(md.H1("B-Tree DAO Members"))
+ sb.WriteString(md.H2("Total NFTs Minted"))
+ sb.WriteString(ufmt.Sprintf("Total NFTs minted: %d\n\n", dao.TokenCount()))
+ sb.WriteString(md.H2("Member Stats"))
+ sb.WriteString(ufmt.Sprintf("Total members: %d\n", totalSize))
+ if biggestTree > 0 {
+ sb.WriteString(ufmt.Sprintf("Biggest tree size: %d\n", biggestTree))
+ }
+ sb.WriteString(md.H2("OG Members"))
+ sb.WriteString(md.BulletList(ogMembers))
+ sb.WriteString(md.H2("Latest Members"))
+ sb.WriteString(md.BulletList(latestMembers))
+
+ return sb.String()
+}
diff --git a/examples/gno.land/r/demo/btree_dao/btree_dao_test.gno b/examples/gno.land/r/demo/btree_dao/btree_dao_test.gno
new file mode 100644
index 00000000000..0514f52f7b4
--- /dev/null
+++ b/examples/gno.land/r/demo/btree_dao/btree_dao_test.gno
@@ -0,0 +1,97 @@
+package btree_dao
+
+import (
+ "std"
+ "strings"
+ "testing"
+ "time"
+
+ "gno.land/p/demo/btree"
+ "gno.land/p/demo/uassert"
+ "gno.land/p/demo/urequire"
+)
+
+func setupTest() {
+ std.TestSetOrigCaller(std.Address("g1ej0qca5ptsw9kfr64ey8jvfy9eacga6mpj2z0y"))
+ members = btree.New()
+}
+
+type TestElement struct {
+ value int
+}
+
+func (te *TestElement) Less(than btree.Record) bool {
+ return te.value < than.(*TestElement).value
+}
+
+func TestPlantTree(t *testing.T) {
+ setupTest()
+
+ tree := btree.New()
+ elements := []int{30, 10, 50, 20, 40}
+ for _, val := range elements {
+ tree.Insert(&TestElement{value: val})
+ }
+
+ err := PlantTree(tree)
+ urequire.NoError(t, err)
+
+ found := false
+ members.Ascend(func(record btree.Record) bool {
+ regDetails := record.(*RegistrationDetails)
+ if regDetails.UserBTree == tree {
+ found = true
+ return false
+ }
+ return true
+ })
+ uassert.True(t, found)
+
+ err = PlantTree(tree)
+ uassert.Error(t, err)
+
+ emptyTree := btree.New()
+ err = PlantTree(emptyTree)
+ uassert.Error(t, err)
+}
+
+func TestPlantSeed(t *testing.T) {
+ setupTest()
+
+ err := PlantSeed("Hello DAO!")
+ urequire.NoError(t, err)
+
+ found := false
+ members.Ascend(func(record btree.Record) bool {
+ regDetails := record.(*RegistrationDetails)
+ if regDetails.UserBTree == nil {
+ found = true
+ uassert.NotEmpty(t, regDetails.NFTID)
+ uassert.True(t, strings.Contains(regDetails.NFTID, "seed_"))
+ return false
+ }
+ return true
+ })
+ uassert.True(t, found)
+
+ err = PlantSeed("")
+ uassert.Error(t, err)
+}
+
+func TestRegistrationDetailsOrdering(t *testing.T) {
+ setupTest()
+
+ rd1 := &RegistrationDetails{
+ Address: std.Address("test1"),
+ RegTime: time.Now(),
+ NFTID: "0",
+ }
+ rd2 := &RegistrationDetails{
+ Address: std.Address("test2"),
+ RegTime: time.Now().Add(time.Hour),
+ NFTID: "1",
+ }
+
+ uassert.True(t, rd1.Less(rd2))
+ uassert.False(t, rd2.Less(rd1))
+}
diff --git a/examples/gno.land/r/demo/btree_dao/gno.mod b/examples/gno.land/r/demo/btree_dao/gno.mod
new file mode 100644
index 00000000000..01b99acc300
--- /dev/null
+++ b/examples/gno.land/r/demo/btree_dao/gno.mod
@@ -0,0 +1 @@
+module gno.land/r/demo/btree_dao
diff --git a/gno.land/pkg/gnoweb/components/breadcrumb.gohtml b/gno.land/pkg/gnoweb/components/breadcrumb.gohtml
index 0118dff5333..9295b17b6f5 100644
--- a/gno.land/pkg/gnoweb/components/breadcrumb.gohtml
+++ b/gno.land/pkg/gnoweb/components/breadcrumb.gohtml
@@ -1,16 +1,16 @@
{{ define "breadcrumb" }}
-
+
{{- range $index, $part := .Parts }}
{{- if $index }}
- -
+
-
{{- else }}
-
-
+
-
{{- end }}
{{ $part.Name }}
{{- end }}
{{- if .Args }}
- -
+
-
{{ .Args }}
{{- end }}
diff --git a/gno.land/pkg/gnoweb/public/styles.css b/gno.land/pkg/gnoweb/public/styles.css
index ce6c8bae639..a6771695454 100644
--- a/gno.land/pkg/gnoweb/public/styles.css
+++ b/gno.land/pkg/gnoweb/public/styles.css
@@ -1,3 +1,3 @@
@font-face{font-family:Roboto;font-style:normal;font-weight:900;font-display:swap;src:url(fonts/roboto/roboto-mono-normal.woff2) format("woff2"),url(fonts/roboto/roboto-mono-normal.woff) format("woff")}@font-face{font-family:Inter var;font-weight:100 900;font-display:block;font-style:oblique 0deg 10deg;src:url(fonts/intervar/Intervar.woff2) format("woff2")}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }
-/*! tailwindcss v3.4.14 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #bdbdbd}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#7c7c7c}input::placeholder,textarea::placeholder{opacity:1;color:#7c7c7c}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}html{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity));font-family:Inter var,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji,sans-serif;font-size:1rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity));font-feature-settings:"kern" on,"liga" on,"calt" on,"zero" on;-webkit-font-feature-settings:"kern" on,"liga" on,"calt" on,"zero" on;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;-moz-osx-font-smoothing:grayscale;font-smoothing:antialiased;font-variant-ligatures:contextual common-ligatures;font-kerning:normal;text-rendering:optimizeLegibility}svg{max-height:100%;max-width:100%}form{margin-top:0;margin-bottom:0}.realm-content{overflow-wrap:break-word;padding-top:2.5rem;font-size:1rem}.realm-content>:first-child{margin-top:0!important}.realm-content a{font-weight:500;--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.realm-content a:hover{text-decoration-line:underline}.realm-content h1,.realm-content h2,.realm-content h3,.realm-content h4{margin-top:3rem;line-height:1.25;--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.realm-content h2,.realm-content h2 *{font-weight:700}.realm-content h3,.realm-content h3 *,.realm-content h4,.realm-content h4 *{font-weight:600}.realm-content h1+h2,.realm-content h2+h3,.realm-content h3+h4{margin-top:1rem}.realm-content h1{font-size:2.375rem;font-weight:700}.realm-content h2{font-size:1.5rem}.realm-content h3{margin-top:2.5rem;font-size:1.25rem}.realm-content h3,.realm-content h4{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-content h4{margin-top:1.5rem;margin-bottom:1.5rem;font-size:1.125rem;font-weight:500}.realm-content p{margin-top:1.25rem;margin-bottom:1.25rem}.realm-content strong{font-weight:700;--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.realm-content strong *{font-weight:700}.realm-content em{font-style:oblique 10deg}.realm-content blockquote{margin-top:1rem;margin-bottom:1rem;border-left-width:4px;--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity));padding-left:1rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity));font-style:oblique 10deg}.realm-content ol,.realm-content ul{margin-top:1.5rem;margin-bottom:1.5rem;padding-left:1rem}.realm-content ol li,.realm-content ul li{margin-bottom:.5rem}.realm-content img{margin-top:2rem;margin-bottom:2rem;max-width:100%}.realm-content figure{margin-top:1.5rem;margin-bottom:1.5rem;text-align:center}.realm-content figcaption{font-size:.875rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-content :not(pre)>code{border-radius:.25rem;background-color:rgb(226 226 226/var(--tw-bg-opacity));padding:.125rem .25rem;font-size:.96em}.realm-content :not(pre)>code,.realm-content pre{--tw-bg-opacity:1;font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.realm-content pre{overflow-x:auto;border-radius:.375rem;background-color:rgb(240 240 240/var(--tw-bg-opacity));padding:1rem}.realm-content hr{margin-top:2.5rem;margin-bottom:2.5rem;border-top-width:1px;--tw-border-opacity:1;border-color:rgb(226 226 226/var(--tw-border-opacity))}.realm-content table{margin-top:2rem;margin-bottom:2rem;width:100%;border-collapse:collapse}.realm-content td,.realm-content th{border-width:1px;--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity));padding:.5rem 1rem}.realm-content th{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity));font-weight:700}.realm-content caption{margin-top:.5rem;text-align:left;font-size:.875rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-content q{margin-top:1.5rem;margin-bottom:1.5rem;border-left-width:4px;--tw-border-opacity:1;border-left-color:rgb(204 204 204/var(--tw-border-opacity));padding-left:1rem;--tw-text-opacity:1;color:rgb(85 85 85/var(--tw-text-opacity));font-style:oblique 10deg;quotes:"“" "”" "‘" "’"}.realm-content q:after,.realm-content q:before{margin-right:.25rem;font-size:1.5rem;--tw-text-opacity:1;color:rgb(153 153 153/var(--tw-text-opacity));content:open-quote;vertical-align:-.4rem}.realm-content q:after{content:close-quote}.realm-content q:before{content:open-quote}.realm-content q:after{content:close-quote}.realm-content ol ol,.realm-content ol ul,.realm-content ul ol,.realm-content ul ul{margin-top:.75rem;margin-bottom:.5rem;padding-left:1rem}.realm-content ul{list-style-type:disc}.realm-content ol{list-style-type:decimal}.realm-content table th:first-child,.realm-content td:first-child{padding-left:0}.realm-content table th:last-child,.realm-content td:last-child{padding-right:0}.realm-content abbr[title]{cursor:help;border-bottom-width:1px;border-style:dotted}.realm-content details{margin-top:1.25rem;margin-bottom:1.25rem}.realm-content summary{cursor:pointer;font-weight:700}.realm-content a code{color:inherit}.realm-content video{margin-top:2rem;margin-bottom:2rem;max-width:100%}.realm-content math{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.realm-content small{font-size:.875rem}.realm-content del{text-decoration-line:line-through}.realm-content sub{vertical-align:sub;font-size:.75rem}.realm-content sup{vertical-align:super;font-size:.75rem}.realm-content button,.realm-content input{border-width:1px;--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity));padding:.5rem 1rem}main :is(h1,h2,h3,h4){scroll-margin-top:6rem}::-moz-selection{--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity));--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}::selection{--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity));--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.sidemenu .peer:checked+label>svg{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.toc-expend-btn:has(#toc-expend:checked)+nav{display:block}.toc-expend-btn:has(#toc-expend:checked) .toc-expend-btn_ico{--tw-rotate:180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.main-header:has(#sidemenu-docs:checked)+main #sidebar #sidebar-docs,.main-header:has(#sidemenu-meta:checked)+main #sidebar #sidebar-meta,.main-header:has(#sidemenu-source:checked)+main #sidebar #sidebar-source,.main-header:has(#sidemenu-summary:checked)+main #sidebar #sidebar-summary{display:block}@media (min-width:40rem){:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked)) .main-navigation,:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked))+main .realm-content{grid-column:span 6/span 6}:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked)) .sidemenu,:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked))+main #sidebar{grid-column:span 4/span 4}}:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked))+main #sidebar:before{position:absolute;top:0;left:-1.75rem;z-index:-1;display:block;height:100%;width:50vw;--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity));--tw-content:"";content:var(--tw-content)}main :is(.source-code)>pre{overflow:scroll;border-radius:.375rem;--tw-bg-opacity:1!important;background-color:rgb(255 255 255/var(--tw-bg-opacity))!important;padding:1rem .25rem;font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;;font-size:.875rem}@media (min-width:40rem){main :is(.source-code)>pre{padding:2rem .75rem;font-size:1rem}}main .realm-content>pre a:hover{text-decoration-line:none}main :is(.realm-content,.source-code)>pre .chroma-ln:target{background-color:transparent!important}main :is(.realm-content,.source-code)>pre .chroma-line:has(.chroma-ln:target),main :is(.realm-content,.source-code)>pre .chroma-line:has(.chroma-ln:target) .chroma-cl,main :is(.realm-content,.source-code)>pre .chroma-line:has(.chroma-lnlinks:hover),main :is(.realm-content,.source-code)>pre .chroma-line:has(.chroma-lnlinks:hover) .chroma-cl{border-radius:.375rem;--tw-bg-opacity:1!important;background-color:rgb(226 226 226/var(--tw-bg-opacity))!important}main :is(.realm-content,.source-code)>pre .chroma-ln{scroll-margin-top:6rem}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.bottom-1{bottom:.25rem}.left-0{left:0}.right-2{right:.5rem}.right-3{right:.75rem}.top-0{top:0}.top-1\/2{top:50%}.top-14{top:3.5rem}.top-2{top:.5rem}.z-max{z-index:9999}.col-span-1{grid-column:span 1/span 1}.col-span-10{grid-column:span 10/span 10}.col-span-3{grid-column:span 3/span 3}.col-span-7{grid-column:span 7/span 7}.row-span-1{grid-row:span 1/span 1}.row-start-1{grid-row-start:1}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-8{margin-bottom:2rem}.mr-10{margin-right:2.5rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-full{height:100%}.max-h-screen{max-height:100vh}.min-h-full{min-height:100%}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-full{width:100%}.min-w-2{min-width:.5rem}.min-w-48{min-width:12rem}.max-w-screen-max{max-width:98.75rem}.shrink-0{flex-shrink:0}.grow-\[2\]{flex-grow:2}.-translate-y-1\/2{--tw-translate-y:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-pointer{cursor:pointer}.list-none{list-style-type:none}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-flow-dense{grid-auto-flow:dense}.auto-rows-min{grid-auto-rows:min-content}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-8{gap:2rem}.gap-x-20{-moz-column-gap:5rem;column-gap:5rem}.gap-x-3{-moz-column-gap:.75rem;column-gap:.75rem}.gap-y-2{row-gap:.5rem}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-scroll{overflow:scroll}.whitespace-pre-wrap{white-space:pre-wrap}.rounded{border-radius:.375rem}.rounded-sm{border-radius:.25rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-t{border-top-width:1px}.border-gray-100{--tw-border-opacity:1;border-color:rgb(226 226 226/var(--tw-border-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.bg-gray-300{--tw-bg-opacity:1;background-color:rgb(153 153 153/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(240 240 240/var(--tw-bg-opacity))}.bg-light{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-transparent{background-color:transparent}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-4{padding:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-10{padding-left:2.5rem;padding-right:2.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-px{padding-top:1px;padding-bottom:1px}.pb-24{padding-bottom:6rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pb-6{padding-bottom:1.5rem}.pb-8{padding-bottom:2rem}.pl-4{padding-left:1rem}.pr-10{padding-right:2.5rem}.pt-0\.5{padding-top:.125rem}.pt-2{padding-top:.5rem}.font-mono{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.text-100{font-size:.875rem}.text-200{font-size:1rem}.text-50{font-size:.75rem}.text-600{font-size:1.5rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.capitalize{text-transform:capitalize}.leading-tight{line-height:1.25}.text-gray-300{--tw-text-opacity:1;color:rgb(153 153 153/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(124 124 124/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgb(19 19 19/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.text-green-600{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.text-light{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.outline-none{outline:2px solid transparent;outline-offset:2px}.text-stroke{-webkit-text-stroke:currentColor;-webkit-text-stroke-width:.6px}.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}.\*\:pl-0>*{padding-left:0}.before\:px-\[0\.18rem\]:before{content:var(--tw-content);padding-left:.18rem;padding-right:.18rem}.before\:text-gray-300:before{content:var(--tw-content);--tw-text-opacity:1;color:rgb(153 153 153/var(--tw-text-opacity))}.before\:content-\[\'\/\'\]:before{--tw-content:"/";content:var(--tw-content)}.before\:content-\[\'\:\'\]:before{--tw-content:":";content:var(--tw-content)}.before\:content-\[\'open\'\]:before{--tw-content:"open";content:var(--tw-content)}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:bottom-0:after{content:var(--tw-content);bottom:0}.after\:left-0:after{content:var(--tw-content);left:0}.after\:block:after{content:var(--tw-content);display:block}.after\:h-1:after{content:var(--tw-content);height:.25rem}.after\:w-full:after{content:var(--tw-content);width:100%}.after\:rounded-t-sm:after{content:var(--tw-content);border-top-left-radius:.25rem;border-top-right-radius:.25rem}.after\:bg-green-600:after{content:var(--tw-content);--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity))}.first\:border-t:first-child{border-top-width:1px}.hover\:border-gray-300:hover{--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.hover\:bg-gray-50:hover{--tw-bg-opacity:1;background-color:rgb(240 240 240/var(--tw-bg-opacity))}.hover\:bg-green-600:hover{--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity))}.hover\:text-gray-600:hover{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.hover\:text-green-600:hover{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.hover\:text-light:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.hover\:underline:hover{text-decoration-line:underline}.focus\:border-gray-300:focus{--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.focus\:border-l-gray-300:focus{--tw-border-opacity:1;border-left-color:rgb(153 153 153/var(--tw-border-opacity))}.group:hover .group-hover\:border-gray-300{--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.group:hover .group-hover\:border-l-gray-300{--tw-border-opacity:1;border-left-color:rgb(153 153 153/var(--tw-border-opacity))}.group.is-active .group-\[\.is-active\]\:text-green-600{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.peer:checked~.peer-checked\:before\:content-\[\'close\'\]:before{--tw-content:"close";content:var(--tw-content)}.peer:focus-within~.peer-focus-within\:hidden{display:none}.has-\[ul\:empty\]\:hidden:has(ul:empty){display:none}.has-\[\:focus-within\]\:border-gray-300:has(:focus-within){--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.has-\[\:focus\]\:border-gray-300:has(:focus){--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}@media (min-width:30rem){.sm\:gap-6{gap:1.5rem}}@media (min-width:40rem){.md\:col-span-3{grid-column:span 3/span 3}.md\:mb-0{margin-bottom:0}.md\:h-4{height:1rem}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:items-center{align-items:center}.md\:gap-x-8{-moz-column-gap:2rem;column-gap:2rem}.md\:px-10{padding-left:2.5rem;padding-right:2.5rem}.md\:pb-0{padding-bottom:0}}@media (min-width:51.25rem){.lg\:order-2{order:2}.lg\:col-span-3{grid-column:span 3/span 3}.lg\:col-span-7{grid-column:span 7/span 7}.lg\:row-span-2{grid-row:span 2/span 2}.lg\:row-start-1{grid-row-start:1}.lg\:row-start-2{grid-row-start:2}.lg\:mb-4{margin-bottom:1rem}.lg\:mt-0{margin-top:0}.lg\:mt-10{margin-top:2.5rem}.lg\:block{display:block}.lg\:hidden{display:none}.lg\:grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:justify-start{justify-content:flex-start}.lg\:justify-between{justify-content:space-between}.lg\:gap-x-20{-moz-column-gap:5rem;column-gap:5rem}.lg\:border-none{border-style:none}.lg\:bg-transparent{background-color:transparent}.lg\:p-0{padding:0}.lg\:px-0{padding-left:0;padding-right:0}.lg\:px-2{padding-left:.5rem;padding-right:.5rem}.lg\:py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.lg\:pb-28{padding-bottom:7rem}.lg\:pt-2{padding-top:.5rem}.lg\:text-200{font-size:1rem}.lg\:font-semibold{font-weight:600}.lg\:hover\:bg-transparent:hover{background-color:transparent}}@media (min-width:63.75rem){.xl\:inline{display:inline}.xl\:hidden{display:none}.xl\:grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.xl\:flex-row{flex-direction:row}.xl\:items-center{align-items:center}.xl\:gap-20{gap:5rem}.xl\:gap-6{gap:1.5rem}.xl\:pt-0{padding-top:0}}@media (min-width:85.375rem){.xxl\:inline-block{display:inline-block}.xxl\:h-4{height:1rem}.xxl\:w-4{width:1rem}.xxl\:gap-20{gap:5rem}.xxl\:gap-x-32{-moz-column-gap:8rem;column-gap:8rem}.xxl\:pr-1{padding-right:.25rem}}
\ No newline at end of file
+/*! tailwindcss v3.4.14 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #bdbdbd}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#7c7c7c}input::placeholder,textarea::placeholder{opacity:1;color:#7c7c7c}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}html{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity));font-family:Inter var,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji,sans-serif;font-size:1rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity));font-feature-settings:"kern" on,"liga" on,"calt" on,"zero" on;-webkit-font-feature-settings:"kern" on,"liga" on,"calt" on,"zero" on;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;-moz-osx-font-smoothing:grayscale;font-smoothing:antialiased;font-variant-ligatures:contextual common-ligatures;font-kerning:normal;text-rendering:optimizeLegibility}svg{max-height:100%;max-width:100%}form{margin-top:0;margin-bottom:0}.realm-content{overflow-wrap:break-word;padding-top:2.5rem;font-size:1rem}.realm-content>:first-child{margin-top:0!important}.realm-content a{font-weight:500;--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.realm-content a:hover{text-decoration-line:underline}.realm-content h1,.realm-content h2,.realm-content h3,.realm-content h4{margin-top:3rem;line-height:1.25;--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.realm-content h2,.realm-content h2 *{font-weight:700}.realm-content h3,.realm-content h3 *,.realm-content h4,.realm-content h4 *{font-weight:600}.realm-content h1+h2,.realm-content h2+h3,.realm-content h3+h4{margin-top:1rem}.realm-content h1{font-size:2.375rem;font-weight:700}.realm-content h2{font-size:1.5rem}.realm-content h3{margin-top:2.5rem;font-size:1.25rem}.realm-content h3,.realm-content h4{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-content h4{margin-top:1.5rem;margin-bottom:1.5rem;font-size:1.125rem;font-weight:500}.realm-content p{margin-top:1.25rem;margin-bottom:1.25rem}.realm-content strong{font-weight:700;--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.realm-content strong *{font-weight:700}.realm-content em{font-style:oblique 10deg}.realm-content blockquote{margin-top:1rem;margin-bottom:1rem;border-left-width:4px;--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity));padding-left:1rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity));font-style:oblique 10deg}.realm-content ol,.realm-content ul{margin-top:1.5rem;margin-bottom:1.5rem;padding-left:1rem}.realm-content ol li,.realm-content ul li{margin-bottom:.5rem}.realm-content img{margin-top:2rem;margin-bottom:2rem;max-width:100%}.realm-content figure{margin-top:1.5rem;margin-bottom:1.5rem;text-align:center}.realm-content figcaption{font-size:.875rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-content :not(pre)>code{border-radius:.25rem;background-color:rgb(226 226 226/var(--tw-bg-opacity));padding:.125rem .25rem;font-size:.96em}.realm-content :not(pre)>code,.realm-content pre{--tw-bg-opacity:1;font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.realm-content pre{overflow-x:auto;border-radius:.375rem;background-color:rgb(240 240 240/var(--tw-bg-opacity));padding:1rem}.realm-content hr{margin-top:2.5rem;margin-bottom:2.5rem;border-top-width:1px;--tw-border-opacity:1;border-color:rgb(226 226 226/var(--tw-border-opacity))}.realm-content table{margin-top:2rem;margin-bottom:2rem;width:100%;border-collapse:collapse}.realm-content td,.realm-content th{border-width:1px;--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity));padding:.5rem 1rem}.realm-content th{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity));font-weight:700}.realm-content caption{margin-top:.5rem;text-align:left;font-size:.875rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-content q{margin-top:1.5rem;margin-bottom:1.5rem;border-left-width:4px;--tw-border-opacity:1;border-left-color:rgb(204 204 204/var(--tw-border-opacity));padding-left:1rem;--tw-text-opacity:1;color:rgb(85 85 85/var(--tw-text-opacity));font-style:oblique 10deg;quotes:"“" "”" "‘" "’"}.realm-content q:after,.realm-content q:before{margin-right:.25rem;font-size:1.5rem;--tw-text-opacity:1;color:rgb(153 153 153/var(--tw-text-opacity));content:open-quote;vertical-align:-.4rem}.realm-content q:after{content:close-quote}.realm-content q:before{content:open-quote}.realm-content q:after{content:close-quote}.realm-content ol ol,.realm-content ol ul,.realm-content ul ol,.realm-content ul ul{margin-top:.75rem;margin-bottom:.5rem;padding-left:1rem}.realm-content ul{list-style-type:disc}.realm-content ol{list-style-type:decimal}.realm-content table th:first-child,.realm-content td:first-child{padding-left:0}.realm-content table th:last-child,.realm-content td:last-child{padding-right:0}.realm-content abbr[title]{cursor:help;border-bottom-width:1px;border-style:dotted}.realm-content details{margin-top:1.25rem;margin-bottom:1.25rem}.realm-content summary{cursor:pointer;font-weight:700}.realm-content a code{color:inherit}.realm-content video{margin-top:2rem;margin-bottom:2rem;max-width:100%}.realm-content math{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.realm-content small{font-size:.875rem}.realm-content del{text-decoration-line:line-through}.realm-content sub{vertical-align:sub;font-size:.75rem}.realm-content sup{vertical-align:super;font-size:.75rem}.realm-content button,.realm-content input{border-width:1px;--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity));padding:.5rem 1rem}main :is(h1,h2,h3,h4){scroll-margin-top:6rem}::-moz-selection{--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity));--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}::selection{--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity));--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.sidemenu .peer:checked+label>svg{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.toc-expend-btn:has(#toc-expend:checked)+nav{display:block}.toc-expend-btn:has(#toc-expend:checked) .toc-expend-btn_ico{--tw-rotate:180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.main-header:has(#sidemenu-docs:checked)+main #sidebar #sidebar-docs,.main-header:has(#sidemenu-meta:checked)+main #sidebar #sidebar-meta,.main-header:has(#sidemenu-source:checked)+main #sidebar #sidebar-source,.main-header:has(#sidemenu-summary:checked)+main #sidebar #sidebar-summary{display:block}@media (min-width:40rem){:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked)) .main-navigation,:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked))+main .realm-content{grid-column:span 6/span 6}:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked)) .sidemenu,:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked))+main #sidebar{grid-column:span 4/span 4}}:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked))+main #sidebar:before{position:absolute;top:0;left:-1.75rem;z-index:-1;display:block;height:100%;width:50vw;--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity));--tw-content:"";content:var(--tw-content)}main :is(.source-code)>pre{overflow:scroll;border-radius:.375rem;--tw-bg-opacity:1!important;background-color:rgb(255 255 255/var(--tw-bg-opacity))!important;padding:1rem .25rem;font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;;font-size:.875rem}@media (min-width:40rem){main :is(.source-code)>pre{padding:2rem .75rem;font-size:1rem}}main .realm-content>pre a:hover{text-decoration-line:none}main :is(.realm-content,.source-code)>pre .chroma-ln:target{background-color:transparent!important}main :is(.realm-content,.source-code)>pre .chroma-line:has(.chroma-ln:target),main :is(.realm-content,.source-code)>pre .chroma-line:has(.chroma-ln:target) .chroma-cl,main :is(.realm-content,.source-code)>pre .chroma-line:has(.chroma-lnlinks:hover),main :is(.realm-content,.source-code)>pre .chroma-line:has(.chroma-lnlinks:hover) .chroma-cl{border-radius:.375rem;--tw-bg-opacity:1!important;background-color:rgb(226 226 226/var(--tw-bg-opacity))!important}main :is(.realm-content,.source-code)>pre .chroma-ln{scroll-margin-top:6rem}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.bottom-1{bottom:.25rem}.left-0{left:0}.right-2{right:.5rem}.right-3{right:.75rem}.top-0{top:0}.top-1\/2{top:50%}.top-14{top:3.5rem}.top-2{top:.5rem}.z-1{z-index:1}.z-max{z-index:9999}.col-span-1{grid-column:span 1/span 1}.col-span-10{grid-column:span 10/span 10}.col-span-3{grid-column:span 3/span 3}.col-span-7{grid-column:span 7/span 7}.row-span-1{grid-row:span 1/span 1}.row-start-1{grid-row-start:1}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-8{margin-bottom:2rem}.mr-10{margin-right:2.5rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-full{height:100%}.max-h-screen{max-height:100vh}.min-h-full{min-height:100%}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-full{width:100%}.min-w-2{min-width:.5rem}.min-w-48{min-width:12rem}.max-w-screen-max{max-width:98.75rem}.shrink-0{flex-shrink:0}.grow-\[2\]{flex-grow:2}.-translate-y-1\/2{--tw-translate-y:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-pointer{cursor:pointer}.list-none{list-style-type:none}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-flow-dense{grid-auto-flow:dense}.auto-rows-min{grid-auto-rows:min-content}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-8{gap:2rem}.gap-x-20{-moz-column-gap:5rem;column-gap:5rem}.gap-x-3{-moz-column-gap:.75rem;column-gap:.75rem}.gap-y-2{row-gap:.5rem}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-scroll{overflow:scroll}.whitespace-pre-wrap{white-space:pre-wrap}.rounded{border-radius:.375rem}.rounded-sm{border-radius:.25rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-t{border-top-width:1px}.border-gray-100{--tw-border-opacity:1;border-color:rgb(226 226 226/var(--tw-border-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.bg-gray-300{--tw-bg-opacity:1;background-color:rgb(153 153 153/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(240 240 240/var(--tw-bg-opacity))}.bg-light{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-transparent{background-color:transparent}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-4{padding:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-10{padding-left:2.5rem;padding-right:2.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-px{padding-top:1px;padding-bottom:1px}.pb-24{padding-bottom:6rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pb-6{padding-bottom:1.5rem}.pb-8{padding-bottom:2rem}.pl-4{padding-left:1rem}.pr-10{padding-right:2.5rem}.pt-0\.5{padding-top:.125rem}.pt-2{padding-top:.5rem}.font-mono{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.text-100{font-size:.875rem}.text-200{font-size:1rem}.text-50{font-size:.75rem}.text-600{font-size:1.5rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.capitalize{text-transform:capitalize}.leading-tight{line-height:1.25}.text-gray-300{--tw-text-opacity:1;color:rgb(153 153 153/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(124 124 124/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgb(19 19 19/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.text-green-600{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.text-light{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.outline-none{outline:2px solid transparent;outline-offset:2px}.text-stroke{-webkit-text-stroke:currentColor;-webkit-text-stroke-width:.6px}.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}.\*\:pl-0>*{padding-left:0}.before\:px-\[0\.18rem\]:before{content:var(--tw-content);padding-left:.18rem;padding-right:.18rem}.before\:text-gray-300:before{content:var(--tw-content);--tw-text-opacity:1;color:rgb(153 153 153/var(--tw-text-opacity))}.before\:content-\[\'\/\'\]:before{--tw-content:"/";content:var(--tw-content)}.before\:content-\[\'\:\'\]:before{--tw-content:":";content:var(--tw-content)}.before\:content-\[\'open\'\]:before{--tw-content:"open";content:var(--tw-content)}.after\:pointer-events-none:after{content:var(--tw-content);pointer-events:none}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:bottom-0:after{content:var(--tw-content);bottom:0}.after\:left-0:after{content:var(--tw-content);left:0}.after\:top-0:after{content:var(--tw-content);top:0}.after\:block:after{content:var(--tw-content);display:block}.after\:h-1:after{content:var(--tw-content);height:.25rem}.after\:h-full:after{content:var(--tw-content);height:100%}.after\:w-full:after{content:var(--tw-content);width:100%}.after\:rounded-t-sm:after{content:var(--tw-content);border-top-left-radius:.25rem;border-top-right-radius:.25rem}.after\:bg-gray-100:after{content:var(--tw-content);--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.after\:bg-green-600:after{content:var(--tw-content);--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity))}.first\:border-t:first-child{border-top-width:1px}.hover\:border-gray-300:hover{--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.hover\:bg-gray-50:hover{--tw-bg-opacity:1;background-color:rgb(240 240 240/var(--tw-bg-opacity))}.hover\:bg-green-600:hover{--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity))}.hover\:text-gray-600:hover{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.hover\:text-green-600:hover{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.hover\:text-light:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.hover\:underline:hover{text-decoration-line:underline}.focus\:border-gray-300:focus{--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.focus\:border-l-gray-300:focus{--tw-border-opacity:1;border-left-color:rgb(153 153 153/var(--tw-border-opacity))}.group:hover .group-hover\:border-gray-300{--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.group:hover .group-hover\:border-l-gray-300{--tw-border-opacity:1;border-left-color:rgb(153 153 153/var(--tw-border-opacity))}.group.is-active .group-\[\.is-active\]\:text-green-600{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.peer:checked~.peer-checked\:before\:content-\[\'close\'\]:before{--tw-content:"close";content:var(--tw-content)}.peer:focus-within~.peer-focus-within\:hidden{display:none}.has-\[ul\:empty\]\:hidden:has(ul:empty){display:none}.has-\[\:focus-within\]\:border-gray-300:has(:focus-within){--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.has-\[\:focus\]\:border-gray-300:has(:focus){--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}@media (min-width:30rem){.sm\:gap-6{gap:1.5rem}}@media (min-width:40rem){.md\:col-span-3{grid-column:span 3/span 3}.md\:mb-0{margin-bottom:0}.md\:h-4{height:1rem}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:items-center{align-items:center}.md\:gap-x-8{-moz-column-gap:2rem;column-gap:2rem}.md\:px-10{padding-left:2.5rem;padding-right:2.5rem}.md\:pb-0{padding-bottom:0}}@media (min-width:51.25rem){.lg\:order-2{order:2}.lg\:col-span-3{grid-column:span 3/span 3}.lg\:col-span-7{grid-column:span 7/span 7}.lg\:row-span-2{grid-row:span 2/span 2}.lg\:row-start-1{grid-row-start:1}.lg\:row-start-2{grid-row-start:2}.lg\:mb-4{margin-bottom:1rem}.lg\:mt-0{margin-top:0}.lg\:mt-10{margin-top:2.5rem}.lg\:block{display:block}.lg\:hidden{display:none}.lg\:grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:justify-start{justify-content:flex-start}.lg\:justify-between{justify-content:space-between}.lg\:gap-x-20{-moz-column-gap:5rem;column-gap:5rem}.lg\:border-none{border-style:none}.lg\:bg-transparent{background-color:transparent}.lg\:p-0{padding:0}.lg\:px-0{padding-left:0;padding-right:0}.lg\:px-2{padding-left:.5rem;padding-right:.5rem}.lg\:py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.lg\:pb-28{padding-bottom:7rem}.lg\:pt-2{padding-top:.5rem}.lg\:text-200{font-size:1rem}.lg\:font-semibold{font-weight:600}.lg\:hover\:bg-transparent:hover{background-color:transparent}}@media (min-width:63.75rem){.xl\:inline{display:inline}.xl\:hidden{display:none}.xl\:grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.xl\:flex-row{flex-direction:row}.xl\:items-center{align-items:center}.xl\:gap-20{gap:5rem}.xl\:gap-6{gap:1.5rem}.xl\:pt-0{padding-top:0}}@media (min-width:85.375rem){.xxl\:inline-block{display:inline-block}.xxl\:h-4{height:1rem}.xxl\:w-4{width:1rem}.xxl\:gap-20{gap:5rem}.xxl\:gap-x-32{-moz-column-gap:8rem;column-gap:8rem}.xxl\:pr-1{padding-right:.25rem}}
\ No newline at end of file
diff --git a/gno.land/pkg/integration/pkgloader.go b/gno.land/pkg/integration/pkgloader.go
index 0a2c0ad62c4..71b1491b2a8 100644
--- a/gno.land/pkg/integration/pkgloader.go
+++ b/gno.land/pkg/integration/pkgloader.go
@@ -130,10 +130,11 @@ func (pl *PkgsLoader) LoadPackage(modroot string, path, name string) error {
if err != nil {
return fmt.Errorf("unable to read package at %q: %w", currentPkg.Dir, err)
}
- imports, err := packages.Imports(pkg, nil)
+ importsMap, err := packages.Imports(pkg, nil)
if err != nil {
return fmt.Errorf("unable to load package imports in %q: %w", currentPkg.Dir, err)
}
+ imports := importsMap.Merge(packages.FileKindPackageSource, packages.FileKindTest, packages.FileKindFiletest)
for _, imp := range imports {
if imp.PkgPath == currentPkg.Name || gnolang.IsStdlib(imp.PkgPath) {
continue
diff --git a/gnovm/cmd/gno/download_deps.go b/gnovm/cmd/gno/download_deps.go
index 5a8c50be20b..4e638eb4970 100644
--- a/gnovm/cmd/gno/download_deps.go
+++ b/gnovm/cmd/gno/download_deps.go
@@ -25,10 +25,11 @@ func downloadDeps(io commands.IO, pkgDir string, gnoMod *gnomod.File, fetcher pk
if err != nil {
return fmt.Errorf("read package at %q: %w", pkgDir, err)
}
- imports, err := packages.Imports(pkg, nil)
+ importsMap, err := packages.Imports(pkg, nil)
if err != nil {
return fmt.Errorf("read imports at %q: %w", pkgDir, err)
}
+ imports := importsMap.Merge(packages.FileKindPackageSource, packages.FileKindTest, packages.FileKindXTest)
for _, pkgPath := range imports {
resolved := gnoMod.Resolve(module.Version{Path: pkgPath.PkgPath})
diff --git a/gnovm/pkg/doc/dirs.go b/gnovm/pkg/doc/dirs.go
index b287fd20708..4c481324e9a 100644
--- a/gnovm/pkg/doc/dirs.go
+++ b/gnovm/pkg/doc/dirs.go
@@ -105,11 +105,12 @@ func packageImportsRecursive(root string, pkgPath string) []string {
pkg = &gnovm.MemPackage{}
}
- resRaw, err := packages.Imports(pkg, nil)
+ importsMap, err := packages.Imports(pkg, nil)
if err != nil {
// ignore packages with invalid imports
- resRaw = nil
+ importsMap = nil
}
+ resRaw := importsMap.Merge(packages.FileKindPackageSource, packages.FileKindTest, packages.FileKindXTest)
res := make([]string, len(resRaw))
for idx, imp := range resRaw {
res[idx] = imp.PkgPath
diff --git a/gnovm/pkg/gnolang/bench_test.go b/gnovm/pkg/gnolang/bench_test.go
new file mode 100644
index 00000000000..b638ab66cd0
--- /dev/null
+++ b/gnovm/pkg/gnolang/bench_test.go
@@ -0,0 +1,31 @@
+package gnolang
+
+import (
+ "testing"
+)
+
+var sink any = nil
+
+var pkgIDPaths = []string{
+ "encoding/json",
+ "math/bits",
+ "github.com/gnolang/gno/gnovm/pkg/gnolang",
+ "a",
+ " ",
+ "",
+ "github.com/gnolang/gno/gnovm/pkg/gnolang/vendor/pkg/github.com/gnolang/vendored",
+}
+
+func BenchmarkPkgIDFromPkgPath(b *testing.B) {
+ b.ReportAllocs()
+ for i := 0; i < b.N; i++ {
+ for _, path := range pkgIDPaths {
+ sink = PkgIDFromPkgPath(path)
+ }
+ }
+
+ if sink == nil {
+ b.Fatal("Benchmark did not run!")
+ }
+ sink = nil
+}
diff --git a/gnovm/pkg/gnolang/gno_test.go b/gnovm/pkg/gnolang/gno_test.go
index 89458667997..5a8c6faf315 100644
--- a/gnovm/pkg/gnolang/gno_test.go
+++ b/gnovm/pkg/gnolang/gno_test.go
@@ -36,6 +36,8 @@ func setupMachine(b *testing.B, numValues, numStmts, numExprs, numBlocks, numFra
func BenchmarkStringLargeData(b *testing.B) {
m := setupMachine(b, 10000, 5000, 5000, 2000, 3000, 1000)
+ b.ResetTimer()
+ b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = m.String()
diff --git a/gnovm/pkg/gnolang/machine.go b/gnovm/pkg/gnolang/machine.go
index 4480a89d16f..75d12ac5402 100644
--- a/gnovm/pkg/gnolang/machine.go
+++ b/gnovm/pkg/gnolang/machine.go
@@ -2117,6 +2117,10 @@ func (m *Machine) Printf(format string, args ...interface{}) {
}
func (m *Machine) String() string {
+ if m == nil {
+ return "Machine:nil"
+ }
+
// Calculate some reasonable total length to avoid reallocation
// Assuming an average length of 32 characters per string
var (
@@ -2131,25 +2135,26 @@ func (m *Machine) String() string {
totalLength = vsLength + ssLength + xsLength + bsLength + obsLength + fsLength + exceptionsLength
)
- var builder strings.Builder
+ var sb strings.Builder
+ builder := &sb // Pointer for use in fmt.Fprintf.
builder.Grow(totalLength)
- builder.WriteString(fmt.Sprintf("Machine:\n PreprocessorMode: %v\n Op: %v\n Values: (len: %d)\n", m.PreprocessorMode, m.Ops[:m.NumOps], m.NumValues))
+ fmt.Fprintf(builder, "Machine:\n PreprocessorMode: %v\n Op: %v\n Values: (len: %d)\n", m.PreprocessorMode, m.Ops[:m.NumOps], m.NumValues)
for i := m.NumValues - 1; i >= 0; i-- {
- builder.WriteString(fmt.Sprintf(" #%d %v\n", i, m.Values[i]))
+ fmt.Fprintf(builder, " #%d %v\n", i, m.Values[i])
}
builder.WriteString(" Exprs:\n")
for i := len(m.Exprs) - 1; i >= 0; i-- {
- builder.WriteString(fmt.Sprintf(" #%d %v\n", i, m.Exprs[i]))
+ fmt.Fprintf(builder, " #%d %v\n", i, m.Exprs[i])
}
builder.WriteString(" Stmts:\n")
for i := len(m.Stmts) - 1; i >= 0; i-- {
- builder.WriteString(fmt.Sprintf(" #%d %v\n", i, m.Stmts[i]))
+ fmt.Fprintf(builder, " #%d %v\n", i, m.Stmts[i])
}
builder.WriteString(" Blocks:\n")
@@ -2166,17 +2171,17 @@ func (m *Machine) String() string {
if pv, ok := b.Source.(*PackageNode); ok {
// package blocks have too much, so just
// print the pkgpath.
- builder.WriteString(fmt.Sprintf(" %s(%d) %s\n", gens, gen, pv.PkgPath))
+ fmt.Fprintf(builder, " %s(%d) %s\n", gens, gen, pv.PkgPath)
} else {
bsi := b.StringIndented(" ")
- builder.WriteString(fmt.Sprintf(" %s(%d) %s\n", gens, gen, bsi))
+ fmt.Fprintf(builder, " %s(%d) %s\n", gens, gen, bsi)
if b.Source != nil {
sb := b.GetSource(m.Store).GetStaticBlock().GetBlock()
- builder.WriteString(fmt.Sprintf(" (s vals) %s(%d) %s\n", gens, gen, sb.StringIndented(" ")))
+ fmt.Fprintf(builder, " (s vals) %s(%d) %s\n", gens, gen, sb.StringIndented(" "))
sts := b.GetSource(m.Store).GetStaticBlock().Types
- builder.WriteString(fmt.Sprintf(" (s typs) %s(%d) %s\n", gens, gen, sts))
+ fmt.Fprintf(builder, " (s typs) %s(%d) %s\n", gens, gen, sts)
}
}
@@ -2187,7 +2192,7 @@ func (m *Machine) String() string {
case *Block:
b = bp
case RefValue:
- builder.WriteString(fmt.Sprintf(" (block ref %v)\n", bp.ObjectID))
+ fmt.Fprintf(builder, " (block ref %v)\n", bp.ObjectID)
b = nil
default:
panic("should not happen")
@@ -2206,12 +2211,12 @@ func (m *Machine) String() string {
if _, ok := b.Source.(*PackageNode); ok {
break // done, skip *PackageNode.
} else {
- builder.WriteString(fmt.Sprintf(" #%d %s\n", i,
- b.StringIndented(" ")))
+ fmt.Fprintf(builder, " #%d %s\n", i,
+ b.StringIndented(" "))
if b.Source != nil {
sb := b.GetSource(m.Store).GetStaticBlock().GetBlock()
- builder.WriteString(fmt.Sprintf(" (static) #%d %s\n", i,
- sb.StringIndented(" ")))
+ fmt.Fprintf(builder, " (static) #%d %s\n", i,
+ sb.StringIndented(" "))
}
}
}
@@ -2219,17 +2224,17 @@ func (m *Machine) String() string {
builder.WriteString(" Frames:\n")
for i := len(m.Frames) - 1; i >= 0; i-- {
- builder.WriteString(fmt.Sprintf(" #%d %s\n", i, m.Frames[i]))
+ fmt.Fprintf(builder, " #%d %s\n", i, m.Frames[i])
}
if m.Realm != nil {
- builder.WriteString(fmt.Sprintf(" Realm:\n %s\n", m.Realm.Path))
+ fmt.Fprintf(builder, " Realm:\n %s\n", m.Realm.Path)
}
builder.WriteString(" Exceptions:\n")
for _, ex := range m.Exceptions {
- builder.WriteString(fmt.Sprintf(" %s\n", ex.Sprint(m)))
+ fmt.Fprintf(builder, " %s\n", ex.Sprint(m))
}
return builder.String()
diff --git a/gnovm/pkg/gnolang/machine_test.go b/gnovm/pkg/gnolang/machine_test.go
index c3b2118f099..c2ab8ea12c5 100644
--- a/gnovm/pkg/gnolang/machine_test.go
+++ b/gnovm/pkg/gnolang/machine_test.go
@@ -9,6 +9,7 @@ import (
"github.com/gnolang/gno/tm2/pkg/store/dbadapter"
"github.com/gnolang/gno/tm2/pkg/store/iavl"
stypes "github.com/gnolang/gno/tm2/pkg/store/types"
+ "github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
)
@@ -56,3 +57,61 @@ func TestRunMemPackageWithOverrides_revertToOld(t *testing.T) {
assert.Equal(t, StringKind, v.T.Kind())
assert.Equal(t, StringValue("1"), v.V)
}
+
+func TestMachineString(t *testing.T) {
+ cases := []struct {
+ name string
+ in *Machine
+ want string
+ }{
+ {
+ "nil Machine",
+ nil,
+ "Machine:nil",
+ },
+ {
+ "created with defaults",
+ NewMachineWithOptions(MachineOptions{}),
+ "Machine:\n PreprocessorMode: false\n Op: []\n Values: (len: 0)\n Exprs:\n Stmts:\n Blocks:\n Blocks (other):\n Frames:\n Exceptions:\n",
+ },
+ {
+ "created with store and defaults",
+ func() *Machine {
+ db := memdb.NewMemDB()
+ baseStore := dbadapter.StoreConstructor(db, stypes.StoreOptions{})
+ iavlStore := iavl.StoreConstructor(db, stypes.StoreOptions{})
+ store := NewStore(nil, baseStore, iavlStore)
+ return NewMachine("std", store)
+ }(),
+ "Machine:\n PreprocessorMode: false\n Op: []\n Values: (len: 0)\n Exprs:\n Stmts:\n Blocks:\n Blocks (other):\n Frames:\n Exceptions:\n",
+ },
+ {
+ "filled in",
+ func() *Machine {
+ db := memdb.NewMemDB()
+ baseStore := dbadapter.StoreConstructor(db, stypes.StoreOptions{})
+ iavlStore := iavl.StoreConstructor(db, stypes.StoreOptions{})
+ store := NewStore(nil, baseStore, iavlStore)
+ m := NewMachine("std", store)
+ m.PushOp(OpHalt)
+ m.PushExpr(&BasicLitExpr{
+ Kind: INT,
+ Value: "100",
+ })
+ m.Blocks = make([]*Block, 1, 1)
+ m.PushStmts(S(Call(X("Redecl"), 11)))
+ return m
+ }(),
+ "Machine:\n PreprocessorMode: false\n Op: [OpHalt]\n Values: (len: 0)\n Exprs:\n #0 100\n Stmts:\n #0 Redecl(11)\n Blocks:\n Blocks (other):\n Frames:\n Exceptions:\n",
+ },
+ }
+
+ for _, tt := range cases {
+ t.Run(tt.name, func(t *testing.T) {
+ got := tt.in.String()
+ if diff := cmp.Diff(got, tt.want); diff != "" {
+ t.Fatalf("Mismatch: got - want +\n%s", diff)
+ }
+ })
+ }
+}
diff --git a/gnovm/pkg/gnolang/realm.go b/gnovm/pkg/gnolang/realm.go
index d822eb290eb..04de760037a 100644
--- a/gnovm/pkg/gnolang/realm.go
+++ b/gnovm/pkg/gnolang/realm.go
@@ -6,6 +6,7 @@ import (
"fmt"
"reflect"
"strings"
+ "sync"
bm "github.com/gnolang/gno/gnovm/pkg/benchops"
)
@@ -71,8 +72,25 @@ func (pid PkgID) Bytes() []byte {
return pid.Hashlet[:]
}
+var (
+ pkgIDFromPkgPathCacheMu sync.Mutex // protects the shared cache.
+ // TODO: later on switch this to an LRU if needed to ensure
+ // fixed memory caps. For now though it isn't a problem:
+ // https://github.com/gnolang/gno/pull/3424#issuecomment-2564571785
+ pkgIDFromPkgPathCache = make(map[string]*PkgID, 100)
+)
+
func PkgIDFromPkgPath(path string) PkgID {
- return PkgID{HashBytes([]byte(path))}
+ pkgIDFromPkgPathCacheMu.Lock()
+ defer pkgIDFromPkgPathCacheMu.Unlock()
+
+ pkgID, ok := pkgIDFromPkgPathCache[path]
+ if !ok {
+ pkgID = new(PkgID)
+ *pkgID = PkgID{HashBytes([]byte(path))}
+ pkgIDFromPkgPathCache[path] = pkgID
+ }
+ return *pkgID
}
// Returns the ObjectID of the PackageValue associated with path.
diff --git a/gnovm/pkg/gnomod/pkg.go b/gnovm/pkg/gnomod/pkg.go
index a0831d494b0..85f1d31442d 100644
--- a/gnovm/pkg/gnomod/pkg.go
+++ b/gnovm/pkg/gnomod/pkg.go
@@ -121,11 +121,13 @@ func ListPkgs(root string) (PkgList, error) {
pkg = &gnovm.MemPackage{}
}
- importsRaw, err := packages.Imports(pkg, nil)
+ importsMap, err := packages.Imports(pkg, nil)
if err != nil {
// ignore imports on error
- importsRaw = nil
+ importsMap = nil
}
+ importsRaw := importsMap.Merge(packages.FileKindPackageSource, packages.FileKindTest, packages.FileKindXTest)
+
imports := make([]string, 0, len(importsRaw))
for _, imp := range importsRaw {
// remove self and standard libraries from imports
diff --git a/gnovm/pkg/packages/filekind.go b/gnovm/pkg/packages/filekind.go
new file mode 100644
index 00000000000..ed2ca84b7d0
--- /dev/null
+++ b/gnovm/pkg/packages/filekind.go
@@ -0,0 +1,56 @@
+package packages
+
+import (
+ "fmt"
+ "go/parser"
+ "go/token"
+ "strings"
+)
+
+// FileKind represent the category a gno source file falls in, can be one of:
+//
+// - [FileKindPackageSource] -> A *.gno file that will be included in the gnovm package
+//
+// - [FileKindTest] -> A *_test.gno file that will be used for testing
+//
+// - [FileKindXTest] -> A *_test.gno file with a package name ending in _test that will be used for blackbox testing
+//
+// - [FileKindFiletest] -> A *_filetest.gno file that will be used for snapshot testing
+type FileKind uint
+
+const (
+ FileKindUnknown FileKind = iota
+ FileKindPackageSource
+ FileKindTest
+ FileKindXTest
+ FileKindFiletest
+)
+
+// GetFileKind analyzes a file's name and body to get it's [FileKind], fset is optional
+func GetFileKind(filename string, body string, fset *token.FileSet) (FileKind, error) {
+ if !strings.HasSuffix(filename, ".gno") {
+ return FileKindUnknown, fmt.Errorf("%s:1:1: not a gno file", filename)
+ }
+
+ if strings.HasSuffix(filename, "_filetest.gno") {
+ return FileKindFiletest, nil
+ }
+
+ if !strings.HasSuffix(filename, "_test.gno") {
+ return FileKindPackageSource, nil
+ }
+
+ if fset == nil {
+ fset = token.NewFileSet()
+ }
+ ast, err := parser.ParseFile(fset, filename, body, parser.PackageClauseOnly)
+ if err != nil {
+ return FileKindUnknown, err
+ }
+ packageName := ast.Name.Name
+
+ if strings.HasSuffix(packageName, "_test") {
+ return FileKindXTest, nil
+ }
+ return FileKindTest, nil
+}
diff --git a/gnovm/pkg/packages/filekind_test.go b/gnovm/pkg/packages/filekind_test.go
new file mode 100644
index 00000000000..bd06b49fb45
--- /dev/null
+++ b/gnovm/pkg/packages/filekind_test.go
@@ -0,0 +1,63 @@
+package packages
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestGetFileKind(t *testing.T) {
+ tcs := []struct {
+ name string
+ filename string
+ body string
+ fileKind FileKind
+ errorContains string
+ }{
+ {
+ name: "compiled",
+ filename: "foo.gno",
+ fileKind: FileKindPackageSource,
+ },
+ {
+ name: "test",
+ filename: "foo_test.gno",
+ body: "package foo",
+ fileKind: FileKindTest,
+ },
+ {
+ name: "xtest",
+ filename: "foo_test.gno",
+ body: "package foo_test",
+ fileKind: FileKindXTest,
+ },
+ {
+ name: "filetest",
+ filename: "foo_filetest.gno",
+ fileKind: FileKindFiletest,
+ },
+ {
+ name: "err_badpkgclause",
+ filename: "foo_test.gno",
+ body: "pakage foo",
+ errorContains: "foo_test.gno:1:1: expected 'package', found pakage",
+ },
+ {
+ name: "err_notgnofile",
+ filename: "foo.gno.bck",
+ errorContains: `foo.gno.bck:1:1: not a gno file`,
+ },
+ }
+
+ for _, tc := range tcs {
+ t.Run(tc.name, func(t *testing.T) {
+ out, err := GetFileKind(tc.filename, tc.body, nil)
+ if len(tc.errorContains) != 0 {
+ require.ErrorContains(t, err, tc.errorContains)
+ } else {
+ require.NoError(t, err)
+ }
+ require.Equal(t, tc.fileKind, out)
+ })
+ }
+}
diff --git a/gnovm/pkg/packages/imports.go b/gnovm/pkg/packages/imports.go
index 201965bc588..3bc60be6664 100644
--- a/gnovm/pkg/packages/imports.go
+++ b/gnovm/pkg/packages/imports.go
@@ -14,33 +14,40 @@ import (
// Imports returns the list of gno imports from a [gnovm.MemPackage].
// fset is optional.
-func Imports(pkg *gnovm.MemPackage, fset *token.FileSet) ([]FileImport, error) {
- allImports := make([]FileImport, 0, 16)
- seen := make(map[string]struct{}, 16)
+func Imports(pkg *gnovm.MemPackage, fset *token.FileSet) (ImportsMap, error) {
+ res := make(ImportsMap, 16)
+ seen := make(map[FileKind]map[string]struct{}, 16)
+
for _, file := range pkg.Files {
if !strings.HasSuffix(file.Name, ".gno") {
continue
}
- if strings.HasSuffix(file.Name, "_filetest.gno") {
- continue
+
+ fileKind, err := GetFileKind(file.Name, file.Body, fset)
+ if err != nil {
+ return nil, err
}
imports, err := FileImports(file.Name, file.Body, fset)
if err != nil {
return nil, err
}
for _, im := range imports {
- if _, ok := seen[im.PkgPath]; ok {
+ if _, ok := seen[fileKind][im.PkgPath]; ok {
continue
}
- allImports = append(allImports, im)
- seen[im.PkgPath] = struct{}{}
+ res[fileKind] = append(res[fileKind], im)
+ if _, ok := seen[fileKind]; !ok {
+ seen[fileKind] = make(map[string]struct{}, 16)
+ }
+ seen[fileKind][im.PkgPath] = struct{}{}
}
}
- sort.Slice(allImports, func(i, j int) bool {
- return allImports[i].PkgPath < allImports[j].PkgPath
- })
- return allImports, nil
+ for _, imports := range res {
+ sortImports(imports)
+ }
+
+ return res, nil
}
// FileImport represents an import
@@ -75,3 +82,31 @@ func FileImports(filename string, src string, fset *token.FileSet) ([]FileImport
}
return res, nil
}
+
+type ImportsMap map[FileKind][]FileImport
+
+// Merge merges imports, it removes duplicates and sorts the result
+func (imap ImportsMap) Merge(kinds ...FileKind) []FileImport {
+ res := make([]FileImport, 0, 16)
+ seen := make(map[string]struct{}, 16)
+
+ for _, kind := range kinds {
+ for _, im := range imap[kind] {
+ if _, ok := seen[im.PkgPath]; ok {
+ continue
+ }
+ seen[im.PkgPath] = struct{}{}
+
+ res = append(res, im)
+ }
+ }
+
+ sortImports(res)
+ return res
+}
+
+func sortImports(imports []FileImport) {
+ sort.Slice(imports, func(i, j int) bool {
+ return imports[i].PkgPath < imports[j].PkgPath
+ })
+}
diff --git a/gnovm/pkg/packages/imports_test.go b/gnovm/pkg/packages/imports_test.go
index 3750aa9108c..f9f58b967c8 100644
--- a/gnovm/pkg/packages/imports_test.go
+++ b/gnovm/pkg/packages/imports_test.go
@@ -58,13 +58,26 @@ func TestImports(t *testing.T) {
)
`,
},
+ {
+ name: "file2_test.gno",
+ data: `
+ package tmp_test
+
+ import (
+ "testing"
+
+ "gno.land/p/demo/testpkg"
+ "gno.land/p/demo/xtestdep"
+ )
+ `,
+ },
{
name: "z_0_filetest.gno",
data: `
package main
import (
- "gno.land/p/demo/filetestpkg"
+ "gno.land/p/demo/filetestdep"
)
`,
},
@@ -95,17 +108,28 @@ func TestImports(t *testing.T) {
},
}
- // Expected list of imports
+ // Expected lists of imports
// - ignore subdirs
// - ignore duplicate
- // - ignore *_filetest.gno
// - should be sorted
- expected := []string{
- "gno.land/p/demo/pkg1",
- "gno.land/p/demo/pkg2",
- "gno.land/p/demo/testpkg",
- "std",
- "testing",
+ expected := map[FileKind][]string{
+ FileKindPackageSource: {
+ "gno.land/p/demo/pkg1",
+ "gno.land/p/demo/pkg2",
+ "std",
+ },
+ FileKindTest: {
+ "gno.land/p/demo/testpkg",
+ "testing",
+ },
+ FileKindXTest: {
+ "gno.land/p/demo/testpkg",
+ "gno.land/p/demo/xtestdep",
+ "testing",
+ },
+ FileKindFiletest: {
+ "gno.land/p/demo/filetestdep",
+ },
}
// Create subpkg dir
@@ -120,12 +144,19 @@ func TestImports(t *testing.T) {
pkg, err := gnolang.ReadMemPackage(tmpDir, "test")
require.NoError(t, err)
- imports, err := Imports(pkg, nil)
+
+ importsMap, err := Imports(pkg, nil)
require.NoError(t, err)
- importsStrings := make([]string, len(imports))
- for idx, imp := range imports {
- importsStrings[idx] = imp.PkgPath
+
+ // ignore specs
+ got := map[FileKind][]string{}
+ for key, vals := range importsMap {
+ stringVals := make([]string, len(vals))
+ for i, val := range vals {
+ stringVals[i] = val.PkgPath
+ }
+ got[key] = stringVals
}
- require.Equal(t, expected, importsStrings)
+ require.Equal(t, expected, got)
}
diff --git a/gnovm/pkg/test/imports.go b/gnovm/pkg/test/imports.go
index 8b24fdeaa77..a8dd709e501 100644
--- a/gnovm/pkg/test/imports.go
+++ b/gnovm/pkg/test/imports.go
@@ -242,10 +242,11 @@ func LoadImports(store gno.Store, memPkg *gnovm.MemPackage) (err error) {
}()
fset := token.NewFileSet()
- imports, err := packages.Imports(memPkg, fset)
+ importsMap, err := packages.Imports(memPkg, fset)
if err != nil {
return err
}
+ imports := importsMap.Merge(packages.FileKindPackageSource, packages.FileKindTest, packages.FileKindXTest)
for _, imp := range imports {
if gno.IsRealmPath(imp.PkgPath) {
// Don't eagerly load realms.
diff --git a/gnovm/pkg/transpiler/fuzz_test.go b/gnovm/pkg/transpiler/fuzz_test.go
index 68414c21e9a..d3ab26eea86 100644
--- a/gnovm/pkg/transpiler/fuzz_test.go
+++ b/gnovm/pkg/transpiler/fuzz_test.go
@@ -1,4 +1,4 @@
-package transpiler
+package transpiler_test
import (
"fmt"
@@ -6,10 +6,12 @@ import (
"io/fs"
"os"
"path/filepath"
- "runtime"
"strings"
"testing"
"time"
+
+ "github.com/gnolang/gno/gnovm/pkg/gnoenv"
+ "github.com/gnolang/gno/gnovm/pkg/transpiler"
)
func FuzzTranspiling(f *testing.F) {
@@ -18,13 +20,7 @@ func FuzzTranspiling(f *testing.F) {
}
// 1. Derive the seeds from our seedGnoFiles.
- breakRoot := filepath.Join("gnolang", "gno")
- pc, thisFile, _, _ := runtime.Caller(0)
- index := strings.Index(thisFile, breakRoot)
- _ = pc // to silence the pedantic golangci linter.
- rootPath := thisFile[:index+len(breakRoot)]
- examplesDir := filepath.Join(rootPath, "examples")
- ffs := os.DirFS(examplesDir)
+ ffs := os.DirFS(filepath.Join(gnoenv.RootDir(), "examples"))
fs.WalkDir(ffs, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
panic(err)
@@ -65,7 +61,7 @@ func FuzzTranspiling(f *testing.F) {
}()
close(readyCh)
defer close(doneCh)
- _, _ = Transpile(string(gnoSourceCode), "gno", "in.gno")
+ _, _ = transpiler.Transpile(string(gnoSourceCode), "gno", "in.gno")
doneCh <- true
}()
diff --git a/go.mod b/go.mod
index 280ca3ae602..cd038e2ae65 100644
--- a/go.mod
+++ b/go.mod
@@ -14,6 +14,7 @@ require (
github.com/davecgh/go-spew v1.1.1
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0
github.com/fortytw2/leaktest v1.3.0
+ github.com/google/go-cmp v0.6.0
github.com/google/gofuzz v1.2.0
github.com/gorilla/websocket v1.5.3
github.com/libp2p/go-buffer-pool v0.1.0
diff --git a/misc/docs-linter/jsx.go b/misc/docs-linter/jsx.go
index d0307680a0c..eb041a78386 100644
--- a/misc/docs-linter/jsx.go
+++ b/misc/docs-linter/jsx.go
@@ -50,7 +50,7 @@ func lintJSX(filepathToJSX map[string][]string) (string, error) {
found = true
}
- output.WriteString(fmt.Sprintf(">>> %s (found in file: %s)\n", tag, filePath))
+ fmt.Fprintf(&output, ">>> %s (found in file: %s)\n", tag, filePath)
}
}
diff --git a/misc/docs-linter/links.go b/misc/docs-linter/links.go
index 744917d8dfb..e34d35d9f58 100644
--- a/misc/docs-linter/links.go
+++ b/misc/docs-linter/links.go
@@ -80,7 +80,7 @@ func lintLocalLinks(filepathToLinks map[string][]string, docsPath string) (strin
found = true
}
- output.WriteString(fmt.Sprintf(">>> %s (found in file: %s)\n", link, filePath))
+ fmt.Fprintf(&output, ">>> %s (found in file: %s)\n", link, filePath)
}
}
}
diff --git a/misc/docs-linter/main.go b/misc/docs-linter/main.go
index 97d80316108..5d7cdf37982 100644
--- a/misc/docs-linter/main.go
+++ b/misc/docs-linter/main.go
@@ -61,8 +61,8 @@ func execLint(cfg *cfg, ctx context.Context) (string, error) {
}
// Main buffer to write to the end user after linting
- var output bytes.Buffer
- output.WriteString(fmt.Sprintf("Linting %s...\n", absPath))
+ var output bytes.Buffer
+ fmt.Fprintf(&output, "Linting %s...\n", absPath)
// Find docs files to lint
mdFiles, err := findFilePaths(cfg.docsPath)
diff --git a/misc/docs-linter/urls.go b/misc/docs-linter/urls.go
index 093e624d81e..098d0a05524 100644
--- a/misc/docs-linter/urls.go
+++ b/misc/docs-linter/urls.go
@@ -66,7 +66,7 @@ func lintURLs(filepathToURLs map[string][]string, ctx context.Context) (string,
found = true
}
- output.WriteString(fmt.Sprintf(">>> %s (found in file: %s)\n", url, filePath))
+ fmt.Fprintf(&output, ">>> %s (found in file: %s)\n", url, filePath)
lock.Unlock()
}
diff --git a/tm2/pkg/bft/rpc/lib/server/handlers.go b/tm2/pkg/bft/rpc/lib/server/handlers.go
index 88ee26da4a9..9e10596a975 100644
--- a/tm2/pkg/bft/rpc/lib/server/handlers.go
+++ b/tm2/pkg/bft/rpc/lib/server/handlers.go
@@ -939,7 +939,7 @@ func writeListOfEndpoints(w http.ResponseWriter, r *http.Request, funcMap map[st
for _, name := range noArgNames {
link := fmt.Sprintf("//%s/%s", r.Host, name)
- buf.WriteString(fmt.Sprintf("%s", link, link))
+ fmt.Fprintf(buf, "%s", link, link)
}
buf.WriteString("
Endpoints that require arguments:
")
@@ -952,7 +952,7 @@ func writeListOfEndpoints(w http.ResponseWriter, r *http.Request, funcMap map[st
link += "&"
}
}
- buf.WriteString(fmt.Sprintf("%s", link, link))
+ fmt.Fprintf(buf, "%s", link, link)
}
buf.WriteString("