From 679fd5da23ddac152d518b7ac2e851a619a529ff Mon Sep 17 00:00:00 2001 From: Thomas Bruyelle Date: Mon, 6 Jul 2026 14:44:48 +0200 Subject: [PATCH 1/3] fix(gnotypes): prevent init panic on Go 1.26+ from recursive builtin type stubs Go 1.26's go/types now treats the self-referential predeclared type stubs in builtin.gno (`type bool bool`, `type comparable interface{ comparable }`, ...) as a fatal "invalid recursive type" error that aborts type resolution for the whole file. This leaves ~23 builtin funcs -- including gno's cross, revive and istypednil -- with a nil Type(), and isMethod's unchecked fn.Type().(*types.Signature) then panics at package init. The panic kills the gnopls binary on startup, so the language server never answers a single request (definition, hover, ...) on any file. On Go 1.24/1.25 the same recursive-type error was reported but non-fatal, so the funcs still got their signatures -- hence the regression only surfaced after building with the 1.26 toolchain. - builtin.gno: drop the self-referential predeclared scalar type stubs and the comparable stub. They are pure documentation: each is already provided by types.Universe and skipped during registration, so removing them changes nothing but lets the file type-check cleanly on all toolchains. - builtin.go: harden isMethod and the builtin registration loop to log-and-skip a nil signature instead of panicking, so a future toolchain change degrades gracefully instead of taking down the whole server. Verified on Go 1.26.4 and 1.25.0: gnotypes/resolver tests pass, builtins register, `gnopls definition` on the realm builtin type and cross-package imports resolve, and `gnopls check` on an interrealm-v2 realm is clean. --- pkg/gnotypes/builtin.go | 19 ++++++- pkg/gnotypes/builtin/builtin.gno | 85 ++++++-------------------------- 2 files changed, 31 insertions(+), 73 deletions(-) diff --git a/pkg/gnotypes/builtin.go b/pkg/gnotypes/builtin.go index 3facdb4..ee66651 100644 --- a/pkg/gnotypes/builtin.go +++ b/pkg/gnotypes/builtin.go @@ -476,6 +476,13 @@ func init() { for name, obj := range gnoBuiltin { switch o := obj.(type) { case *types.Func: + // Skip funcs whose signature failed to type-check (nil Type). + // See isMethod for why this can happen. + origSig, ok := o.Type().(*types.Signature) + if !ok { + log.Printf("builtin func %q skipped: signature did not type-check", o.Name()) + continue + } // Clone the signature with the shared ctx so any custom builtin // named types it references resolve to the same package-less // instances registered in the Universe below — not the @@ -483,7 +490,7 @@ func init() { // Without this, cross(cur) fails to type-check: cross's param // would be builtin.realm while the Universe realm is the cloned, // package-less type (identity mismatch). - sig := ctx.CloneTypeWithNilPackage(o.Type()).(*types.Signature) + sig := ctx.CloneTypeWithNilPackage(origSig).(*types.Signature) newFn := types.NewFunc(token.NoPos, nil, name, sig) // a builtin don't have a pos types.Universe.Insert(newFn) // register func log.Printf("builtin func %q has been registered", o.Name()) @@ -510,6 +517,14 @@ func isMethod(obj types.Object) bool { if !ok { return false } + // A func whose signature failed to type-check has a nil Type (e.g. when a + // recursive-type error elsewhere in the builtin file aborts resolution). + // Treat it as a non-method so callers can filter it out instead of + // panicking on the type assertion below. + sig, ok := fn.Type().(*types.Signature) + if !ok { + return false + } // Check if the function has a receiver (i.e., is a method) - return fn.Type().(*types.Signature).Recv() != nil + return sig.Recv() != nil } diff --git a/pkg/gnotypes/builtin/builtin.gno b/pkg/gnotypes/builtin/builtin.gno index 043a239..59ced25 100644 --- a/pkg/gnotypes/builtin/builtin.gno +++ b/pkg/gnotypes/builtin/builtin.gno @@ -13,8 +13,16 @@ import "cmp" for the language's special identifiers. */ -// bool is the set of boolean values, true and false. -type bool bool +// NOTE: The predeclared scalar types (bool, the sized ints/uints, floats, +// complex, string, int, uint, uintptr) and the `comparable` constraint are +// intentionally NOT redeclared here. Go's own builtin.go documents them with +// self-referential stubs such as `type bool bool`, but go/types rejects those +// as an "invalid recursive type" error. On Go 1.26+ that error aborts type +// resolution for the whole file, leaving every builtin func (including gno's +// cross/revive/istypednil) with a nil signature — which crashed gnopls at +// init. These identifiers are already provided by types.Universe and are +// skipped during registration regardless, so the stubs served no purpose here +// beyond documentation. // true and false are the two untyped boolean values. const ( @@ -22,69 +30,6 @@ const ( false = 0 != 0 // Untyped bool. ) -// uint8 is the set of all unsigned 8-bit integers. -// Range: 0 through 255. -type uint8 uint8 - -// uint16 is the set of all unsigned 16-bit integers. -// Range: 0 through 65535. -type uint16 uint16 - -// uint32 is the set of all unsigned 32-bit integers. -// Range: 0 through 4294967295. -type uint32 uint32 - -// uint64 is the set of all unsigned 64-bit integers. -// Range: 0 through 18446744073709551615. -type uint64 uint64 - -// int8 is the set of all signed 8-bit integers. -// Range: -128 through 127. -type int8 int8 - -// int16 is the set of all signed 16-bit integers. -// Range: -32768 through 32767. -type int16 int16 - -// int32 is the set of all signed 32-bit integers. -// Range: -2147483648 through 2147483647. -type int32 int32 - -// int64 is the set of all signed 64-bit integers. -// Range: -9223372036854775808 through 9223372036854775807. -type int64 int64 - -// float32 is the set of all IEEE 754 32-bit floating-point numbers. -type float32 float32 - -// float64 is the set of all IEEE 754 64-bit floating-point numbers. -type float64 float64 - -// complex64 is the set of all complex numbers with float32 real and -// imaginary parts. -type complex64 complex64 - -// complex128 is the set of all complex numbers with float64 real and -// imaginary parts. -type complex128 complex128 - -// string is the set of all strings of 8-bit bytes, conventionally but not -// necessarily representing UTF-8-encoded text. A string may be empty, but -// not nil. Values of string type are immutable. -type string string - -// int is a signed integer type that is at least 32 bits in size. It is a -// distinct type, however, and not an alias for, say, int32. -type int int - -// uint is an unsigned integer type that is at least 32 bits in size. It is a -// distinct type, however, and not an alias for, say, uint32. -type uint uint - -// uintptr is an integer type that is large enough to hold the bit pattern of -// any pointer. -type uintptr uintptr - // byte is an alias for uint8 and is equivalent to uint8 in all ways. It is // used, by convention, to distinguish byte values from 8-bit unsigned // integer values. @@ -97,12 +42,10 @@ type rune = int32 // any is an alias for interface{} and is equivalent to interface{} in all ways. type any = interface{} -// comparable is an interface that is implemented by all comparable types -// (booleans, numbers, strings, pointers, channels, arrays of comparable types, -// structs whose fields are all comparable types). -// The comparable interface may only be used as a type parameter constraint, -// not as the type of a variable. -type comparable interface{ comparable } +// comparable is intentionally omitted here; see the note above. It is +// provided by types.Universe and its self-referential documentation stub +// (`type comparable interface{ comparable }`) triggers the same recursive-type +// error. // iota is a predeclared identifier representing the untyped integer ordinal // number of the current const specification in a (usually parenthesized) From f70a77309146113e36ecb89bade369bb18e5b2c7 Mon Sep 17 00:00:00 2001 From: Thomas Bruyelle Date: Mon, 6 Jul 2026 14:49:56 +0200 Subject: [PATCH 2/3] build: pin toolchain to go1.26.4 Add a `toolchain go1.26.4` directive so CI and `go install` build with the Go 1.26 toolchain that exposes the gnotypes init panic fixed in the previous commit. Without this, a machine with an older default toolchain could build a working binary while 1.26 users get a crash-on-startup server, and CI would stay green against the very version that breaks. The `go 1.25.0` minimum language version is left unchanged. --- go.mod | 2 ++ 1 file changed, 2 insertions(+) diff --git a/go.mod b/go.mod index 2a462a1..4daef60 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,8 @@ module github.com/gnoverse/gnopls go 1.25.0 +toolchain go1.26.4 + require ( github.com/gnolang/gno v0.0.0-20260415100849-375fe89c6d20 github.com/google/go-cmp v0.7.0 From 2cab3c5c9801fe6fad5df32f817e1ed2006a45de Mon Sep 17 00:00:00 2001 From: Thomas Bruyelle Date: Mon, 6 Jul 2026 16:18:45 +0200 Subject: [PATCH 3/3] fix(gnotypes): guard IsGnoBuiltin against a nil signature Apply the same defensive handling as isMethod to IsGnoBuiltin's obj.Type().(*types.Signature) assertion: an unresolved func signature now returns false instead of panicking. Addresses PR review feedback (the last unguarded instance of this pattern). --- pkg/gnotypes/builtin.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/gnotypes/builtin.go b/pkg/gnotypes/builtin.go index ee66651..a7c7672 100644 --- a/pkg/gnotypes/builtin.go +++ b/pkg/gnotypes/builtin.go @@ -114,9 +114,12 @@ func IsGnoBuiltin(obj types.Object) bool { switch obj.(type) { case *types.Func: - // lookup for function only - if obj.Type().(*types.Signature).Recv() != nil { - return false // method + // lookup for function only. A nil/non-signature type means the func + // failed to type-check; treat it as not a builtin (same defensive + // handling as isMethod) rather than panicking on the assertion. + sig, ok := obj.Type().(*types.Signature) + if !ok || sig.Recv() != nil { + return false // unresolved signature or method } case *types.TypeName: