Description
The GnoVM preprocessor incorrectly identifies a group of type declarations that is valid in Go as an invalid recursive type and panics. The declarations form cycles mixing aliases and defined types. They are valid in Go because each cycle passes through both an indirection (slice/pointer) and a defined type.
Type alias syntax has been supported since Go 1.9, and cycle checking for mixed cycles of this kind has been supported since the go/types rewrite in Go 1.18. go build, go vet, and go/types accept this code, but it subsequently fails during GnoVM preprocessing.
Reference: golang/go#25838
Environment
- Go:
go1.26.2
- OS/architecture:
darwin/arm64
- Gno commit:
fb02547fd9066ff50a7aa6f2d6469ee1275c5b19
Steps to Reproduce
Run the following code.
It is adapted from the go/types test case for golang/go#25838 and moved into the main package.
package main
type (
e = f
f = g
g = []h
h i
i = j
j = e
)
type (
e1 = []h1
h1 e1
)
type (
P = *T
T P
)
func main() {}
gno playground: https://play.gno.land/p/81s5Y1EUq1d
go playground: https://go.dev/play/p/fTVwZ_UnWex
Expected Behavior
Each cycle is valid because it passes through both a slice/pointer indirection and a defined (non-alias) type. The program should exit without errors or output. go build, go vet, and go/types all accept this code.
Validity requires both of the following conditions:
- The cycle passes through a slice, pointer, map, or similar indirection, so the type does not have infinite size (arrays and structs are not indirections).
- The cycle contains at least one defined type, so the alias chain resolves to a concrete type.
Boundary cases verified with go/types:
| Code |
Result |
Reason |
type A [3]A |
Rejected |
It is a defined type, but the array provides no indirection. |
type A = []A |
Rejected |
There is slice indirection, but every type is an alias (no defined type). |
type (A = []B; B A) |
Accepted |
Slice indirection plus the defined type B. |
type (A = []B; B = A) |
Rejected |
Every type is an alias. |
The groups in the reproduction are valid because they pass through []h (indirection) and h/h1/T (defined types).
Actual Behavior
The GnoVM preprocessor incorrectly identifies the first declaration group as an invalid recursive type and panics.
gno run gnovm/tests/files/fixedbugs/02.gno
panic: invalid recursive type: e -> f -> g -> h -> i -> j -> e [recovered]
panic: main/gnovm/tests/files/fixedbugs/02.gno:13:2-7: invalid recursive type: e -> f -> g -> h -> i -> j -> e:
--- preprocess stack ---
stack 0: file main/gnovm/tests/files/fixedbugs/02.gno:8:1-31:15
------------------------
goroutine 1 [running]:
github.com/gnolang/gno/gnovm/pkg/gnolang.doRecover({0x1b98d00c8e10, 0x1, 0x1}, {0x103a1d348, 0x1b98d0af8820})
/Users/notjoon/gno-core/gnovm/pkg/gnolang/preprocess.go:639 +0x254
panic({0x103815420?, 0x1b98d00c8da0?})
/usr/local/go/src/runtime/panic.go:860 +0x12c
github.com/gnolang/gno/gnovm/pkg/gnolang.findUndefinedAny({0x103a29d98, 0x1b98d0658700}, {0x103a2a698, 0x1b98d0679808}, {0x103a1f380?, 0x1b98d03ef420}, {0x1b98d04be200, 0x6, 0x8}, 0x1b98d0b1f218, ...)
/Users/notjoon/gno-core/gnovm/pkg/gnolang/preprocess.go:5039 +0xc84
github.com/gnolang/gno/gnovm/pkg/gnolang.tryPredefine({0x103a29d98, 0x1b98d0658700}, 0x1b98d0679408, {0x103a2a698, 0x1b98d0679808}, {0x103a20f08, 0x1b98d0af8c30}, {0x1b98d04be200, 0x6, 0x8}, ...)
/Users/notjoon/gno-core/gnovm/pkg/gnolang/preprocess.go:5633 +0x1f74
github.com/gnolang/gno/gnovm/pkg/gnolang.predefineRecursively2({0x103a29d98, 0x1b98d0658700}, {0x103a2a698, 0x1b98d0679808}, {0x103a20f08, 0x1b98d0af8c30}, {0x1b98d04be200, 0x5, 0x8}, 0x1b98d0b1f218, ...)
/Users/notjoon/gno-core/gnovm/pkg/gnolang/preprocess.go:5384 +0x2c4
github.com/gnolang/gno/gnovm/pkg/gnolang.predefineRecursively2({0x103a29d98, 0x1b98d0658700}, {0x103a2a698, 0x1b98d0679808}, {0x103a20f08, 0x1b98d0af8b60}, {0x1b98d0592040, 0x4, 0x4}, 0x1b98d0b1f218, ...)
/Users/notjoon/gno-core/gnovm/pkg/gnolang/preprocess.go:5408 +0x280
github.com/gnolang/gno/gnovm/pkg/gnolang.predefineRecursively2({0x103a29d98, 0x1b98d0658700}, {0x103a2a698, 0x1b98d0679808}, {0x103a20f08, 0x1b98d0af8a90}, {0x1b98d0592040, 0x3, 0x4}, 0x1b98d0b1f218, ...)
/Users/notjoon/gno-core/gnovm/pkg/gnolang/preprocess.go:5408 +0x280
github.com/gnolang/gno/gnovm/pkg/gnolang.predefineRecursively2({0x103a29d98, 0x1b98d0658700}, {0x103a2a698, 0x1b98d0679808}, {0x103a20f08, 0x1b98d0af89c0}, {0x1b98d0b1f050, 0x2, 0x2}, 0x1b98d0b1f218, ...)
/Users/notjoon/gno-core/gnovm/pkg/gnolang/preprocess.go:5408 +0x280
github.com/gnolang/gno/gnovm/pkg/gnolang.predefineRecursively2({0x103a29d98, 0x1b98d0658700}, {0x103a2a698, 0x1b98d0679808}, {0x103a20f08, 0x1b98d0af88f0}, {0x1b98d0b1f050, 0x1, 0x2}, 0x1b98d0b1f218, ...)
/Users/notjoon/gno-core/gnovm/pkg/gnolang/preprocess.go:5408 +0x280
github.com/gnolang/gno/gnovm/pkg/gnolang.predefineRecursively2({0x103a29d98, 0x1b98d0658700}, {0x103a2a698, 0x1b98d0679808}, {0x103a20f08, 0x1b98d0af8820}, {0x1b98d0b1f148, 0x0, 0x0}, 0x1b98d0b1f218, ...)
/Users/notjoon/gno-core/gnovm/pkg/gnolang/preprocess.go:5408 +0x280
github.com/gnolang/gno/gnovm/pkg/gnolang.predefineRecursively({0x103a29d98, 0x1b98d0658700}, {0x103a2a698, 0x1b98d0679808}, {0x103a20f08, 0x1b98d0af8820})
/Users/notjoon/gno-core/gnovm/pkg/gnolang/preprocess.go:5344 +0x104
github.com/gnolang/gno/gnovm/pkg/gnolang.PredefineFileSet({0x103a29d98, 0x1b98d0658700}, 0x1b98d0679408, 0x1b98d0089968)
/Users/notjoon/gno-core/gnovm/pkg/gnolang/preprocess.go:83 +0x3cc
github.com/gnolang/gno/gnovm/pkg/gnolang.(*Machine).runFileDecls(0x1b98d0492708, 0x1, {0x1b98d0182540, 0x1, 0x1})
/Users/notjoon/gno-core/gnovm/pkg/gnolang/machine.go:762 +0x1e0
github.com/gnolang/gno/gnovm/pkg/gnolang.(*Machine).RunFiles(0x1b98d0492708, {0x1b98d0182540, 0x1, 0x1})
/Users/notjoon/gno-core/gnovm/pkg/gnolang/machine.go:640 +0x11c
main.execRun(0x1b98d0af40f0, {0x1b98d00c85e0, 0x1, 0x1}, {0x103a1bbe8, 0x1b98d0af40a0})
/Users/notjoon/gno-core/gnovm/cmd/gno/run.go:299 +0x594
main.newRunCmd.func1({0x1b98d0b1fdb8?, 0x1021babcc?}, {0x1b98d00c85e0?, 0x16dc62e20?, 0x1b98d0af2570?})
/Users/notjoon/gno-core/gnovm/cmd/gno/run.go:41 +0x38
github.com/gnolang/gno/tm2/pkg/commands.(*Command).Run(0x1b98d01e4540?, {0x103a0e230?, 0x103bcca60?})
/Users/notjoon/gno-core/tm2/pkg/commands/command.go:265 +0x17c
github.com/gnolang/gno/tm2/pkg/commands.(*Command).Run(0x1b98d01e4000?, {0x103a0e230?, 0x103bcca60?})
/Users/notjoon/gno-core/tm2/pkg/commands/command.go:269 +0x138
main.main()
/Users/notjoon/gno-core/gnovm/cmd/gno/main.go:49 +0x194
Cause
findUndefinedAny in preprocess.go uses the direct flag for cycle detection. A slice or pointer indirection should clear this flag and break the cycle, but it does not do so in an alias context.
SliceTypeExpr: direct = astype && isalias
StarExpr: direct = isalias
As a result, when []h is encountered in the alias chain e = f = g = []h, direct remains set. Traversal continues into the slice element h (which is actually a defined type), and e -> f -> g -> h -> i -> j -> e is incorrectly treated as recursion.
Suggested fix: slice, pointer, and map indirections should break the cycle even in an alias context. However, alias-only cycles with no defined type, such as type A = A or type (A = []A), must still be rejected.
Impact
- Type alias programs that are valid in Go cannot be run in GnoVM.
- The results of
go/types validation and GnoVM preprocessing are inconsistent.
- Slice and pointer indirections in an alias context are not recognized as cycle-breaking points, causing valid code to be misidentified as recursive types.
Description
The GnoVM preprocessor incorrectly identifies a group of type declarations that is valid in Go as an
invalid recursive typeand panics. The declarations form cycles mixing aliases and defined types. They are valid in Go because each cycle passes through both an indirection (slice/pointer) and a defined type.Type alias syntax has been supported since Go 1.9, and cycle checking for mixed cycles of this kind has been supported since the
go/typesrewrite in Go 1.18.go build,go vet, andgo/typesaccept this code, but it subsequently fails during GnoVM preprocessing.Reference: golang/go#25838
Environment
go1.26.2darwin/arm64fb02547fd9066ff50a7aa6f2d6469ee1275c5b19Steps to Reproduce
Run the following code.
It is adapted from the
go/typestest case forgolang/go#25838and moved into themainpackage.gno playground: https://play.gno.land/p/81s5Y1EUq1d
go playground: https://go.dev/play/p/fTVwZ_UnWex
Expected Behavior
Each cycle is valid because it passes through both a slice/pointer indirection and a defined (non-alias) type. The program should exit without errors or output.
go build,go vet, andgo/typesall accept this code.Validity requires both of the following conditions:
Boundary cases verified with
go/types:type A [3]Atype A = []Atype (A = []B; B A)B.type (A = []B; B = A)The groups in the reproduction are valid because they pass through
[]h(indirection) andh/h1/T(defined types).Actual Behavior
The GnoVM preprocessor incorrectly identifies the first declaration group as an invalid recursive type and panics.
Cause
findUndefinedAnyinpreprocess.gouses thedirectflag for cycle detection. A slice or pointer indirection should clear this flag and break the cycle, but it does not do so in an alias context.SliceTypeExpr:direct = astype && isaliasStarExpr:direct = isaliasAs a result, when
[]his encountered in the alias chaine = f = g = []h,directremains set. Traversal continues into the slice elementh(which is actually a defined type), ande -> f -> g -> h -> i -> j -> eis incorrectly treated as recursion.Suggested fix: slice, pointer, and map indirections should break the cycle even in an alias context. However, alias-only cycles with no defined type, such as
type A = Aortype (A = []A), must still be rejected.Impact
go/typesvalidation and GnoVM preprocessing are inconsistent.