Skip to content

Valid Cyclic Type Alias Declarations Are Incorrectly Rejected as Recursive Types #6036

Description

@notJoon

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions