From cd621226c7183ed3c74dd86575d37a7534ee316f Mon Sep 17 00:00:00 2001 From: ltzMaxwell Date: Mon, 1 Jun 2026 23:34:08 +0800 Subject: [PATCH 01/19] test(gnovm): add range_blank_key filetest for nil-key range assertion --- gnovm/tests/files/range_blank_key.gno | 31 +++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 gnovm/tests/files/range_blank_key.gno diff --git a/gnovm/tests/files/range_blank_key.gno b/gnovm/tests/files/range_blank_key.gno new file mode 100644 index 00000000000..7c2455c3c18 --- /dev/null +++ b/gnovm/tests/files/range_blank_key.gno @@ -0,0 +1,31 @@ +package main + +// Regression: `for _ = range slice/array` panicked +// "runtime error: invalid memory address or nil pointer dereference" +// from inside (*RangeStmt).AssertCompatible -> assertIndexTypeIsInt(nil) +// at gnolang/type_check.go. evalStaticTypeOf returns nil for a blank +// key; the SliceType/ArrayType branches called kt.Kind() without the +// nil guard the StringKind branch already had. +// +// Repro reduced from Go corpus fixedbugs/bug406.go. + +type matrix struct { + e []int +} + +func (a matrix) equal() bool { + for _ = range a.e { + } + for range a.e { + } + return true +} + +func main() { + var a matrix + var i interface{} + i = true && a.equal() + _ = i +} + +// Output: From 72007ce3396a1151e20b3e488e7c2a45f8431fe9 Mon Sep 17 00:00:00 2001 From: ltzMaxwell Date: Mon, 1 Jun 2026 23:34:08 +0800 Subject: [PATCH 02/19] fix(gnovm): guard nil kt in assertIndexTypeIsInt for blank-key range --- gnovm/pkg/gnolang/type_check.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/gnovm/pkg/gnolang/type_check.go b/gnovm/pkg/gnolang/type_check.go index af175dc1ef2..275e4fdafce 100644 --- a/gnovm/pkg/gnolang/type_check.go +++ b/gnovm/pkg/gnolang/type_check.go @@ -820,6 +820,13 @@ func (x *IncDecStmt) AssertCompatible(t Type) { } func assertIndexTypeIsInt(kt Type) { + // kt is nil when the range key is the blank identifier `_` — no + // lvalue is bound, so nothing to type-check. Mirrors the existing + // `kt != nil` guard in the string-range branch of + // (*RangeStmt).AssertCompatible. + if kt == nil { + return + } if kt.Kind() != IntKind { panic(fmt.Sprintf("index type should be int, but got %v", kt)) } From fa5c11d3f2d1d29d22f8af82d210d55f50e40f8b Mon Sep 17 00:00:00 2001 From: ltzMaxwell Date: Tue, 2 Jun 2026 00:06:11 +0800 Subject: [PATCH 03/19] chore(gnovm): tighten range_blank_key fix and filetest --- gnovm/pkg/gnolang/type_check.go | 8 +------- gnovm/tests/files/range_blank_key.gno | 28 ++++----------------------- 2 files changed, 5 insertions(+), 31 deletions(-) diff --git a/gnovm/pkg/gnolang/type_check.go b/gnovm/pkg/gnolang/type_check.go index 275e4fdafce..e83938641e1 100644 --- a/gnovm/pkg/gnolang/type_check.go +++ b/gnovm/pkg/gnolang/type_check.go @@ -820,10 +820,6 @@ func (x *IncDecStmt) AssertCompatible(t Type) { } func assertIndexTypeIsInt(kt Type) { - // kt is nil when the range key is the blank identifier `_` — no - // lvalue is bound, so nothing to type-check. Mirrors the existing - // `kt != nil` guard in the string-range branch of - // (*RangeStmt).AssertCompatible. if kt == nil { return } @@ -868,9 +864,7 @@ func (x *RangeStmt) AssertCompatible(store Store, last BlockNode) { } case PrimitiveType: if cxt.Kind() == StringKind { - if kt != nil && kt.Kind() != IntKind { - panic(fmt.Sprintf("index type should be int, but got %v", kt)) - } + assertIndexTypeIsInt(kt) if vt != nil { if vt.Kind() != Int32Kind { // rune panic(fmt.Sprintf("value type should be int32, but got %v", kt)) diff --git a/gnovm/tests/files/range_blank_key.gno b/gnovm/tests/files/range_blank_key.gno index 7c2455c3c18..2906144ac51 100644 --- a/gnovm/tests/files/range_blank_key.gno +++ b/gnovm/tests/files/range_blank_key.gno @@ -1,31 +1,11 @@ package main -// Regression: `for _ = range slice/array` panicked -// "runtime error: invalid memory address or nil pointer dereference" -// from inside (*RangeStmt).AssertCompatible -> assertIndexTypeIsInt(nil) -// at gnolang/type_check.go. evalStaticTypeOf returns nil for a blank -// key; the SliceType/ArrayType branches called kt.Kind() without the -// nil guard the StringKind branch already had. -// -// Repro reduced from Go corpus fixedbugs/bug406.go. - -type matrix struct { - e []int -} - -func (a matrix) equal() bool { - for _ = range a.e { - } - for range a.e { - } - return true -} +// Regression: `for _ = range slice` used to panic on nil key in assertIndexTypeIsInt. func main() { - var a matrix - var i interface{} - i = true && a.equal() - _ = i + s := []int{1, 2, 3} + for _ = range s { + } } // Output: From 5e72fed3d339e07cdf89033b9eef0cb86c3e084a Mon Sep 17 00:00:00 2001 From: ltzMaxwell Date: Wed, 10 Jun 2026 12:38:07 +0800 Subject: [PATCH 04/19] fix(gnovm): handle blank range key/value per-operand in AssertCompatible --- gnovm/pkg/gnolang/type_check.go | 25 ++++++++++--------- gnovm/tests/files/range_blank_key.gno | 11 --------- gnovm/tests/files/types/range_blank_key2.gno | 14 +++++++++++ gnovm/tests/files/types/range_blank_key3.gno | 15 +++++++++++ gnovm/tests/files/types/range_blank_key4.gno | 14 +++++++++++ gnovm/tests/files/types/range_blank_key5.gno | 26 ++++++++++++++++++++ 6 files changed, 83 insertions(+), 22 deletions(-) delete mode 100644 gnovm/tests/files/range_blank_key.gno create mode 100644 gnovm/tests/files/types/range_blank_key2.gno create mode 100644 gnovm/tests/files/types/range_blank_key3.gno create mode 100644 gnovm/tests/files/types/range_blank_key4.gno create mode 100644 gnovm/tests/files/types/range_blank_key5.gno diff --git a/gnovm/pkg/gnolang/type_check.go b/gnovm/pkg/gnolang/type_check.go index e51e4e1e84d..52583cbd3e3 100644 --- a/gnovm/pkg/gnolang/type_check.go +++ b/gnovm/pkg/gnolang/type_check.go @@ -832,23 +832,26 @@ func (x *RangeStmt) AssertCompatible(store Store, last BlockNode) { if x.Op != ASSIGN { return } - if isBlankIdentifier(x.Key) && (x.Value == nil || isBlankIdentifier(x.Value)) { - // both "_" or key is "_" and value is not present - return + // A blank or absent operand has no static type and nothing to check; + // handle each operand independently, like AssignStmt.AssertCompatible. + var kt, vt Type + if !isBlankIdentifier(x.Key) { + assertValidAssignLhs(store, last, x.Key) + kt = evalStaticTypeOf(store, last, x.Key) } - assertValidAssignLhs(store, last, x.Key) - // if is valid left value - - kt := evalStaticTypeOf(store, last, x.Key) - var vt Type - if x.Value != nil { + if x.Value != nil && !isBlankIdentifier(x.Value) { vt = evalStaticTypeOf(store, last, x.Value) } + if kt == nil && vt == nil { + return + } xt := evalStaticTypeOf(store, last, x.X) switch cxt := xt.(type) { case *MapType: - mustAssignableTo(x, cxt.Key, kt) + if kt != nil { + mustAssignableTo(x, cxt.Key, kt) + } if vt != nil { mustAssignableTo(x, cxt.Value, vt) } @@ -867,7 +870,7 @@ func (x *RangeStmt) AssertCompatible(store Store, last BlockNode) { assertIndexTypeIsInt(kt) if vt != nil { if vt.Kind() != Int32Kind { // rune - panic(fmt.Sprintf("value type should be int32, but got %v", kt)) + panic(fmt.Sprintf("value type should be int32, but got %v", vt)) } } } diff --git a/gnovm/tests/files/range_blank_key.gno b/gnovm/tests/files/range_blank_key.gno deleted file mode 100644 index 2906144ac51..00000000000 --- a/gnovm/tests/files/range_blank_key.gno +++ /dev/null @@ -1,11 +0,0 @@ -package main - -// Regression: `for _ = range slice` used to panic on nil key in assertIndexTypeIsInt. - -func main() { - s := []int{1, 2, 3} - for _ = range s { - } -} - -// Output: diff --git a/gnovm/tests/files/types/range_blank_key2.gno b/gnovm/tests/files/types/range_blank_key2.gno new file mode 100644 index 00000000000..628a0436c16 --- /dev/null +++ b/gnovm/tests/files/types/range_blank_key2.gno @@ -0,0 +1,14 @@ +// Regression: `for _, v = range slice` used to panic on nil key type in +// assertIndexTypeIsInt. +package main + +func main() { + a := []int{1, 2, 3} + var v int + for _, v = range a { + } + println(v) +} + +// Output: +// 3 diff --git a/gnovm/tests/files/types/range_blank_key3.gno b/gnovm/tests/files/types/range_blank_key3.gno new file mode 100644 index 00000000000..9bebdcdf670 --- /dev/null +++ b/gnovm/tests/files/types/range_blank_key3.gno @@ -0,0 +1,15 @@ +// Regression: `for _ = range slice` used to panic on nil key type in +// assertIndexTypeIsInt. +package main + +func main() { + s := []int{1, 2, 3} + n := 0 + for _ = range s { + n++ + } + println(n) +} + +// Output: +// 3 diff --git a/gnovm/tests/files/types/range_blank_key4.gno b/gnovm/tests/files/types/range_blank_key4.gno new file mode 100644 index 00000000000..151a376fb73 --- /dev/null +++ b/gnovm/tests/files/types/range_blank_key4.gno @@ -0,0 +1,14 @@ +// Blank key and value over array, in the assign (not define) form. +package main + +func main() { + a := [3]int{1, 2, 3} + n := 0 + for _, _ = range a { + n++ + } + println(n) +} + +// Output: +// 3 diff --git a/gnovm/tests/files/types/range_blank_key5.gno b/gnovm/tests/files/types/range_blank_key5.gno new file mode 100644 index 00000000000..9639d325be6 --- /dev/null +++ b/gnovm/tests/files/types/range_blank_key5.gno @@ -0,0 +1,26 @@ +// Blank key with real value over map and string, in the assign form. +package main + +func main() { + m := map[string]int{"a": 1} + var v int + for _, v = range m { + } + println(v) + + var r rune + for _, r = range "ab" { + } + println(string(r)) + + for _ = range m { + } + for _ = range "ab" { + } + println("ok") +} + +// Output: +// 1 +// b +// ok From c15e60b2251a80ce20dda2d65016a5c0a41566bc Mon Sep 17 00:00:00 2001 From: ltzMaxwell Date: Wed, 10 Jun 2026 12:56:31 +0800 Subject: [PATCH 05/19] refactor(gnovm): rely on blank-type convention in map range check, flatten string check --- gnovm/pkg/gnolang/type_check.go | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/gnovm/pkg/gnolang/type_check.go b/gnovm/pkg/gnolang/type_check.go index 52583cbd3e3..8d033bc1d82 100644 --- a/gnovm/pkg/gnolang/type_check.go +++ b/gnovm/pkg/gnolang/type_check.go @@ -820,7 +820,7 @@ func (x *IncDecStmt) AssertCompatible(t Type) { } func assertIndexTypeIsInt(kt Type) { - if kt == nil { + if kt == nil { // blank key, nothing to check return } if kt.Kind() != IntKind { @@ -849,9 +849,7 @@ func (x *RangeStmt) AssertCompatible(store Store, last BlockNode) { xt := evalStaticTypeOf(store, last, x.X) switch cxt := xt.(type) { case *MapType: - if kt != nil { - mustAssignableTo(x, cxt.Key, kt) - } + mustAssignableTo(x, cxt.Key, kt) if vt != nil { mustAssignableTo(x, cxt.Value, vt) } @@ -868,10 +866,8 @@ func (x *RangeStmt) AssertCompatible(store Store, last BlockNode) { case PrimitiveType: if cxt.Kind() == StringKind { assertIndexTypeIsInt(kt) - if vt != nil { - if vt.Kind() != Int32Kind { // rune - panic(fmt.Sprintf("value type should be int32, but got %v", vt)) - } + if vt != nil && vt.Kind() != Int32Kind { // rune + panic(fmt.Sprintf("value type should be int32, but got %v", vt)) } } } From fadc5fa0485f27b40c5b759f60d9299ff660bd04 Mon Sep 17 00:00:00 2001 From: ltzMaxwell Date: Wed, 10 Jun 2026 13:15:28 +0800 Subject: [PATCH 06/19] refactor(gnovm): drop redundant vt nil guards in range type checks --- gnovm/pkg/gnolang/type_check.go | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/gnovm/pkg/gnolang/type_check.go b/gnovm/pkg/gnolang/type_check.go index 8d033bc1d82..47fe9463b9e 100644 --- a/gnovm/pkg/gnolang/type_check.go +++ b/gnovm/pkg/gnolang/type_check.go @@ -850,19 +850,13 @@ func (x *RangeStmt) AssertCompatible(store Store, last BlockNode) { switch cxt := xt.(type) { case *MapType: mustAssignableTo(x, cxt.Key, kt) - if vt != nil { - mustAssignableTo(x, cxt.Value, vt) - } + mustAssignableTo(x, cxt.Value, vt) case *SliceType: assertIndexTypeIsInt(kt) - if vt != nil { - mustAssignableTo(x, cxt.Elt, vt) - } + mustAssignableTo(x, cxt.Elt, vt) case *ArrayType: assertIndexTypeIsInt(kt) - if vt != nil { - mustAssignableTo(x, cxt.Elt, vt) - } + mustAssignableTo(x, cxt.Elt, vt) case PrimitiveType: if cxt.Kind() == StringKind { assertIndexTypeIsInt(kt) From af1d4baf9c8b282ce1b81789277b4590da12022d Mon Sep 17 00:00:00 2001 From: ltzMaxwell Date: Wed, 10 Jun 2026 13:30:29 +0800 Subject: [PATCH 07/19] refactor(gnovm): hoist blank-target nil case to top of checkAssignableTo --- gnovm/pkg/gnolang/type_check.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/gnovm/pkg/gnolang/type_check.go b/gnovm/pkg/gnolang/type_check.go index 47fe9463b9e..38dd5af4ad7 100644 --- a/gnovm/pkg/gnolang/type_check.go +++ b/gnovm/pkg/gnolang/type_check.go @@ -385,9 +385,15 @@ func checkAssignableTo(n Node, xt, dt Type) (err error) { if debug { debug.Printf("checkAssignableTo, xt: %v dt: %v \n", xt, dt) } + // A nil dt means the assignment target is discarded: a blank identifier + // (`_ = xxx`, assign8.gno, 0f31) or a blank/absent range operand. + // Anything is assignable to it. + if dt == nil { + return nil + } // case0 - if xt == nil { // see test/files/types/eql_0f18 - if dt == nil || dt.Kind() == InterfaceKind { + if xt == nil { // untyped nil, see test/files/types/eql_0f18 + if dt.Kind() == InterfaceKind { return nil } if !mayBeNil(dt) { @@ -407,8 +413,6 @@ func checkAssignableTo(n Node, xt, dt Type) (err error) { } } return nil - } else if dt == nil { // _ = xxx, assign8.gno, 0f31. else cases? - return nil } // case3 if dt.Kind() == InterfaceKind { // note native interface From 20da0c7305d4c0df4bb7847be87671f34cc4af4c Mon Sep 17 00:00:00 2001 From: ltzMaxwell Date: Thu, 11 Jun 2026 10:05:55 +0800 Subject: [PATCH 08/19] test(gnovm): add assign_range_f/g filetests (const as range value operand) --- gnovm/tests/files/types/assign_range_f.gno | 16 ++++++++++++++++ gnovm/tests/files/types/assign_range_g.gno | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 gnovm/tests/files/types/assign_range_f.gno create mode 100644 gnovm/tests/files/types/assign_range_g.gno diff --git a/gnovm/tests/files/types/assign_range_f.gno b/gnovm/tests/files/types/assign_range_f.gno new file mode 100644 index 00000000000..018684f07a1 --- /dev/null +++ b/gnovm/tests/files/types/assign_range_f.gno @@ -0,0 +1,16 @@ +package main + +const c int = 5 + +func main() { + a := []int{1, 2, 3} + for _, c = range a { + } + println("ok") +} + +// Error: +// main/assign_range_f.gno:7:2-8:3: cannot assign to (const (5 int)) + +// TypeCheckError: +// main/assign_range_f.gno:7:9: cannot assign to c (neither addressable nor a map index expression) diff --git a/gnovm/tests/files/types/assign_range_g.gno b/gnovm/tests/files/types/assign_range_g.gno new file mode 100644 index 00000000000..c2826bf9fb7 --- /dev/null +++ b/gnovm/tests/files/types/assign_range_g.gno @@ -0,0 +1,16 @@ +package main + +const c = 5 + +func main() { + a := []int{1, 2, 3} + for _, c = range a { + } + println("ok") +} + +// Error: +// main/assign_range_g.gno:7:2-8:3: cannot assign to (const (5 bigint)) + +// TypeCheckError: +// main/assign_range_g.gno:7:9: cannot assign to c (neither addressable nor a map index expression) From f2e16f128b495d62a065d09ef9018322873414dc Mon Sep 17 00:00:00 2001 From: ltzMaxwell Date: Thu, 11 Jun 2026 10:05:56 +0800 Subject: [PATCH 09/19] fix(gnovm): reject unassignable range value operand in AssertCompatible --- gnovm/pkg/gnolang/type_check.go | 1 + 1 file changed, 1 insertion(+) diff --git a/gnovm/pkg/gnolang/type_check.go b/gnovm/pkg/gnolang/type_check.go index 38dd5af4ad7..405583ce3cb 100644 --- a/gnovm/pkg/gnolang/type_check.go +++ b/gnovm/pkg/gnolang/type_check.go @@ -844,6 +844,7 @@ func (x *RangeStmt) AssertCompatible(store Store, last BlockNode) { kt = evalStaticTypeOf(store, last, x.Key) } if x.Value != nil && !isBlankIdentifier(x.Value) { + assertValidAssignLhs(store, last, x.Value) vt = evalStaticTypeOf(store, last, x.Value) } if kt == nil && vt == nil { From b77da58ccadae703619baea55202ba457b5a1c61 Mon Sep 17 00:00:00 2001 From: ltzMaxwell Date: Thu, 11 Jun 2026 10:27:08 +0800 Subject: [PATCH 10/19] test(gnovm): add assign_op_a/b and incdec_a5 filetests (unassignable compound-assign and inc/dec targets) --- gnovm/tests/files/types/assign_op_a.gno | 14 ++++++++++++++ gnovm/tests/files/types/assign_op_b.gno | 13 +++++++++++++ gnovm/tests/files/types/incdec_a5.gno | 14 ++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 gnovm/tests/files/types/assign_op_a.gno create mode 100644 gnovm/tests/files/types/assign_op_b.gno create mode 100644 gnovm/tests/files/types/incdec_a5.gno diff --git a/gnovm/tests/files/types/assign_op_a.gno b/gnovm/tests/files/types/assign_op_a.gno new file mode 100644 index 00000000000..b2a8e407a57 --- /dev/null +++ b/gnovm/tests/files/types/assign_op_a.gno @@ -0,0 +1,14 @@ +package main + +const c int = 5 + +func main() { + c += 1 + println("ok") +} + +// Error: +// main/assign_op_a.gno:6:2-8: cannot assign to const c + +// TypeCheckError: +// main/assign_op_a.gno:6:2: cannot assign to c (neither addressable nor a map index expression) diff --git a/gnovm/tests/files/types/assign_op_b.gno b/gnovm/tests/files/types/assign_op_b.gno new file mode 100644 index 00000000000..a911570c6a0 --- /dev/null +++ b/gnovm/tests/files/types/assign_op_b.gno @@ -0,0 +1,13 @@ +package main + +func main() { + s := "abc" + s[0] += 1 + println("ok") +} + +// Error: +// main/assign_op_b.gno:5:2-11: cannot assign to s[(const (0 int))] + +// TypeCheckError: +// main/assign_op_b.gno:5:2: cannot assign to s[0] (neither addressable nor a map index expression) diff --git a/gnovm/tests/files/types/incdec_a5.gno b/gnovm/tests/files/types/incdec_a5.gno new file mode 100644 index 00000000000..00e770ffc95 --- /dev/null +++ b/gnovm/tests/files/types/incdec_a5.gno @@ -0,0 +1,14 @@ +package main + +const c int = 5 + +func main() { + c++ + println("ok") +} + +// Error: +// main/incdec_a5.gno:6:2-5: cannot assign to (const (5 int)) + +// TypeCheckError: +// main/incdec_a5.gno:6:2: cannot assign to c (neither addressable nor a map index expression) From f7564342da62dcd9dfc9baaca66d2c7d85b4d4f5 Mon Sep 17 00:00:00 2001 From: ltzMaxwell Date: Thu, 11 Jun 2026 10:27:08 +0800 Subject: [PATCH 11/19] fix(gnovm): validate assign target for compound assign and inc/dec statements --- gnovm/pkg/gnolang/preprocess.go | 1 + gnovm/pkg/gnolang/type_check.go | 3 +++ 2 files changed, 4 insertions(+) diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index 7387746b3fb..f439bf2613b 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -2878,6 +2878,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { case *IncDecStmt: xt := evalStaticTypeOf(store, last, n.X) n.AssertCompatible(xt) + assertValidAssignLhs(store, last, n.X) // TRANS_LEAVE ----------------------- case *ForStmt: diff --git a/gnovm/pkg/gnolang/type_check.go b/gnovm/pkg/gnolang/type_check.go index 405583ce3cb..dc2deb8ba14 100644 --- a/gnovm/pkg/gnolang/type_check.go +++ b/gnovm/pkg/gnolang/type_check.go @@ -1011,6 +1011,9 @@ func (x *AssignStmt) AssertCompatible(store Store, last BlockNode) { } else { panic(fmt.Sprintf("checker for %s does not exist", x.Op)) } + // like go/types, operator/type errors take precedence over + // target assignability. + assertValidAssignLhs(store, last, x.Lhs[0]) } } From d56157a0cbf55776182513c1f2ecfd2d1b609697 Mon Sep 17 00:00:00 2001 From: ltzMaxwell Date: Thu, 11 Jun 2026 12:32:25 +0800 Subject: [PATCH 12/19] test(gnovm): add assign_nil3 filetest (_ = nil must be rejected) --- gnovm/tests/files/types/assign_nil3.gno | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 gnovm/tests/files/types/assign_nil3.gno diff --git a/gnovm/tests/files/types/assign_nil3.gno b/gnovm/tests/files/types/assign_nil3.gno new file mode 100644 index 00000000000..f2117b70afb --- /dev/null +++ b/gnovm/tests/files/types/assign_nil3.gno @@ -0,0 +1,11 @@ +package main + +func main() { + _ = nil +} + +// Error: +// main/assign_nil3.gno:4:2-9: use of untyped nil in assignment + +// TypeCheckError: +// main/assign_nil3.gno:4:6: use of untyped nil in assignment to _ identifier From 6c5896067c6d9fb045c833d7a335e8dc183cb31f Mon Sep 17 00:00:00 2001 From: ltzMaxwell Date: Thu, 11 Jun 2026 12:32:25 +0800 Subject: [PATCH 13/19] fix(gnovm): reject bare untyped nil assigned to blank identifier --- gnovm/pkg/gnolang/type_check.go | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/gnovm/pkg/gnolang/type_check.go b/gnovm/pkg/gnolang/type_check.go index 38dd5af4ad7..e5aa112d461 100644 --- a/gnovm/pkg/gnolang/type_check.go +++ b/gnovm/pkg/gnolang/type_check.go @@ -387,7 +387,8 @@ func checkAssignableTo(n Node, xt, dt Type) (err error) { } // A nil dt means the assignment target is discarded: a blank identifier // (`_ = xxx`, assign8.gno, 0f31) or a blank/absent range operand. - // Anything is assignable to it. + // Anything is assignable to it. (`_ = nil` is rejected earlier, in + // assertValidAssignRhs; here `T(nil)` is indistinguishable from `nil`.) if dt == nil { return nil } @@ -1050,7 +1051,7 @@ func assertValidAssignRhs(store Store, last BlockNode, n Node) { panic(fmt.Sprintf("unexpected node type %T", n)) } - for _, exp := range exps { + for i, exp := range exps { tt := evalStaticTypeOfRaw(store, last, exp) if tt == nil { switch x := n.(type) { @@ -1061,6 +1062,11 @@ func assertValidAssignRhs(store Store, last BlockNode, n Node) { panic("use of untyped nil in variable declaration") case *AssignStmt: if x.Op != DEFINE { + // `v = nil` is checked against v's type later, but a + // blank target has no type to convert nil to: `_ = nil`. + if i < len(x.Lhs) && isBlankIdentifier(x.Lhs[i]) && isNilIdentifier(exp) { + panic("use of untyped nil in assignment") + } continue } panic("use of untyped nil in assignment") @@ -1161,6 +1167,19 @@ func isBlankIdentifier(x Expr) bool { return false } +// isNilIdentifier reports whether x is the bare `nil` keyword, possibly +// const-folded; a conversion like `T(nil)` also has a nil static type but is +// not bare nil. +func isNilIdentifier(x Expr) bool { + if cx, ok := x.(*ConstExpr); ok { + x = cx.Source + } + if nx, ok := x.(*NameExpr); ok { + return nx.Name == "nil" + } + return false +} + // isComparable returns true if the type can be compared with ==. // This is used for map key validation and other comparability checks. func isComparable(dt Type) bool { From 7c99b1492450eae5aadcff43a30f29ac5f08e904 Mon Sep 17 00:00:00 2001 From: ltzMaxwell Date: Thu, 11 Jun 2026 12:51:54 +0800 Subject: [PATCH 14/19] refactor(gnovm): require non-nil dt in checkAssignableTo, skip blank targets at callers --- gnovm/pkg/gnolang/preprocess.go | 12 +++++--- gnovm/pkg/gnolang/type_check.go | 46 +++++++++++++++++++--------- gnovm/pkg/gnolang/type_check_test.go | 24 +++++++++++---- 3 files changed, 57 insertions(+), 25 deletions(-) diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index 7387746b3fb..1e38a962322 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -4715,8 +4715,10 @@ func checkOrConvertType(store Store, last BlockNode, n Node, x *Expr, t Type) { debug.Printf("checkOrConvertType, *x: %v:, t:%v \n", *x, t) } if cx, ok := (*x).(*ConstExpr); ok { - // e.g. int(1) == int8(1) - mustAssignableTo(n, cx.T, t) + if t != nil { + // e.g. int(1) == int8(1) + mustAssignableTo(n, cx.T, t) + } } else if bx, ok := (*x).(*BinaryExpr); ok && (bx.Op == SHL || bx.Op == SHR) { xt := evalStaticTypeOf(store, last, *x) if debug { @@ -4733,7 +4735,7 @@ func checkOrConvertType(store Store, last BlockNode, n Node, x *Expr, t Type) { // Convert untyped to typed. checkOrConvertType(store, last, n, &bx.Left, t) bx.SetAttribute(ATTR_TYPEOF_VALUE, t) // propagate converted type from left operand to shift expr. - } else { + } else if t != nil { mustAssignableTo(n, xt, t) } return @@ -4780,7 +4782,9 @@ func checkOrConvertType(store Store, last BlockNode, n Node, x *Expr, t Type) { } else if ux, ok := (*x).(*UnaryExpr); ok { xt := evalStaticTypeOf(store, last, *x) // check assignable first - mustAssignableTo(n, xt, t) + if t != nil { + mustAssignableTo(n, xt, t) + } if t == nil || t.Kind() == InterfaceKind { t = defaultTypeOf(xt) diff --git a/gnovm/pkg/gnolang/type_check.go b/gnovm/pkg/gnolang/type_check.go index e5aa112d461..8bbc74371d4 100644 --- a/gnovm/pkg/gnolang/type_check.go +++ b/gnovm/pkg/gnolang/type_check.go @@ -385,12 +385,13 @@ func checkAssignableTo(n Node, xt, dt Type) (err error) { if debug { debug.Printf("checkAssignableTo, xt: %v dt: %v \n", xt, dt) } - // A nil dt means the assignment target is discarded: a blank identifier - // (`_ = xxx`, assign8.gno, 0f31) or a blank/absent range operand. - // Anything is assignable to it. (`_ = nil` is rejected earlier, in - // assertValidAssignRhs; here `T(nil)` is indistinguishable from `nil`.) + // Assignability is defined only between types. A blank/absent target + // (`_ = xxx`, blank range operands) has no static type — callers must + // skip the check syntactically (isBlankIdentifier) instead of passing + // nil, otherwise an accidental nil (e.g. an untyped-nil lhs) would be + // silently accepted. if dt == nil { - return nil + panic("should not happen: nil dt in checkAssignableTo (blank targets must be skipped by the caller)") } // case0 if xt == nil { // untyped nil, see test/files/types/eql_0f18 @@ -824,10 +825,8 @@ func (x *IncDecStmt) AssertCompatible(t Type) { } } +// kt must be non-nil: callers skip blank keys. func assertIndexTypeIsInt(kt Type) { - if kt == nil { // blank key, nothing to check - return - } if kt.Kind() != IntKind { panic(fmt.Sprintf("index type should be int, but got %v", kt)) } @@ -854,17 +853,31 @@ func (x *RangeStmt) AssertCompatible(store Store, last BlockNode) { xt := evalStaticTypeOf(store, last, x.X) switch cxt := xt.(type) { case *MapType: - mustAssignableTo(x, cxt.Key, kt) - mustAssignableTo(x, cxt.Value, vt) + if kt != nil { + mustAssignableTo(x, cxt.Key, kt) + } + if vt != nil { + mustAssignableTo(x, cxt.Value, vt) + } case *SliceType: - assertIndexTypeIsInt(kt) - mustAssignableTo(x, cxt.Elt, vt) + if kt != nil { + assertIndexTypeIsInt(kt) + } + if vt != nil { + mustAssignableTo(x, cxt.Elt, vt) + } case *ArrayType: - assertIndexTypeIsInt(kt) - mustAssignableTo(x, cxt.Elt, vt) + if kt != nil { + assertIndexTypeIsInt(kt) + } + if vt != nil { + mustAssignableTo(x, cxt.Elt, vt) + } case PrimitiveType: if cxt.Kind() == StringKind { - assertIndexTypeIsInt(kt) + if kt != nil { + assertIndexTypeIsInt(kt) + } if vt != nil && vt.Kind() != Int32Kind { // rune panic(fmt.Sprintf("value type should be int32, but got %v", vt)) } @@ -963,6 +976,9 @@ func (x *AssignStmt) AssertCompatible(store Store, last BlockNode) { // assert valid left value for i, lx := range x.Lhs { assertValidAssignLhs(store, last, lx) + if isBlankIdentifier(lx) { + continue // no static type; nothing to check + } lt := evalStaticTypeOf(store, last, lx) rt := evalStaticTypeOf(store, last, x.Rhs[i]) mustAssignableTo(x, rt, lt) diff --git a/gnovm/pkg/gnolang/type_check_test.go b/gnovm/pkg/gnolang/type_check_test.go index fedb3e0ba3e..a1caad0983b 100644 --- a/gnovm/pkg/gnolang/type_check_test.go +++ b/gnovm/pkg/gnolang/type_check_test.go @@ -10,11 +10,15 @@ func TestCheckAssignableTo(t *testing.T) { xt Type dt Type wantError string + wantPanic bool }{ { - name: "nil to nil", - xt: nil, - dt: nil, + // nil dt means a blank target; callers must skip the check + // instead of passing nil. + name: "nil to nil", + xt: nil, + dt: nil, + wantPanic: true, }, { name: "nil and interface", @@ -22,9 +26,10 @@ func TestCheckAssignableTo(t *testing.T) { dt: &InterfaceType{}, }, { - name: "interface to nil", - xt: &InterfaceType{}, - dt: nil, + name: "interface to nil", + xt: &InterfaceType{}, + dt: nil, + wantPanic: true, }, { name: "nil to non-nillable", @@ -43,6 +48,13 @@ func TestCheckAssignableTo(t *testing.T) { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() + if tt.wantPanic { + defer func() { + if r := recover(); r == nil { + t.Errorf("checkAssignableTo() should panic on nil dt") + } + }() + } err := checkAssignableTo(nil, tt.xt, tt.dt) if tt.wantError != "" { if err.Error() != tt.wantError { From e3318658afc8115643f88b8a6c86dc6d5146ef1d Mon Sep 17 00:00:00 2001 From: ltzMaxwell Date: Thu, 11 Jun 2026 13:27:32 +0800 Subject: [PATCH 15/19] test(gnovm): add assign_range_h filetest (nil as range value operand) --- gnovm/tests/files/types/assign_range_h.gno | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 gnovm/tests/files/types/assign_range_h.gno diff --git a/gnovm/tests/files/types/assign_range_h.gno b/gnovm/tests/files/types/assign_range_h.gno new file mode 100644 index 00000000000..3660faade16 --- /dev/null +++ b/gnovm/tests/files/types/assign_range_h.gno @@ -0,0 +1,18 @@ +// `nil` as the range value operand: its static type is nil, aliasing with a +// blank operand at the type level — only the syntactic lhs check catches it. +package main + +func main() { + m := map[string]int{"a": 1} + var k string + for k, nil = range m { + } + _ = k + println("ok") +} + +// Error: +// main/assign_range_h.gno:8:2-9:3: cannot assign to (const (undefined)) + +// TypeCheckError: +// main/assign_range_h.gno:8:9: cannot assign to nil (neither addressable nor a map index expression) From dc75a8a10e989e9951d08fc21272c8efb9b63367 Mon Sep 17 00:00:00 2001 From: ltzMaxwell Date: Thu, 11 Jun 2026 13:29:31 +0800 Subject: [PATCH 16/19] docs(gnovm): explain nil-dt ambiguity on checkAssignableTo panic --- gnovm/pkg/gnolang/type_check.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gnovm/pkg/gnolang/type_check.go b/gnovm/pkg/gnolang/type_check.go index 8bbc74371d4..c0baa5b03b6 100644 --- a/gnovm/pkg/gnolang/type_check.go +++ b/gnovm/pkg/gnolang/type_check.go @@ -388,8 +388,9 @@ func checkAssignableTo(n Node, xt, dt Type) (err error) { // Assignability is defined only between types. A blank/absent target // (`_ = xxx`, blank range operands) has no static type — callers must // skip the check syntactically (isBlankIdentifier) instead of passing - // nil, otherwise an accidental nil (e.g. an untyped-nil lhs) would be - // silently accepted. + // nil. A nil dt is ambiguous: an untyped-nil lvalue also has a nil + // static type (e.g. `for k, nil = range m`), so accepting nil here + // would silently skip the check for it instead of rejecting it. if dt == nil { panic("should not happen: nil dt in checkAssignableTo (blank targets must be skipped by the caller)") } From 764d0b88db55b70b294b49e67026b666ecb3a17d Mon Sep 17 00:00:00 2001 From: ltzMaxwell Date: Thu, 11 Jun 2026 16:01:48 +0800 Subject: [PATCH 17/19] refactor(gnovm): panic explicitly on nil kt in assertIndexTypeIsInt --- gnovm/pkg/gnolang/type_check.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gnovm/pkg/gnolang/type_check.go b/gnovm/pkg/gnolang/type_check.go index c0baa5b03b6..f84f80754c7 100644 --- a/gnovm/pkg/gnolang/type_check.go +++ b/gnovm/pkg/gnolang/type_check.go @@ -826,8 +826,10 @@ func (x *IncDecStmt) AssertCompatible(t Type) { } } -// kt must be non-nil: callers skip blank keys. func assertIndexTypeIsInt(kt Type) { + if kt == nil { + panic("should not happen: nil kt in assertIndexTypeIsInt (blank keys must be skipped by the caller)") + } if kt.Kind() != IntKind { panic(fmt.Sprintf("index type should be int, but got %v", kt)) } From 935a2dc0ad55a0bbb11f69f07df35bdf4a1b3eec Mon Sep 17 00:00:00 2001 From: ltzMaxwell Date: Thu, 11 Jun 2026 18:46:42 +0800 Subject: [PATCH 18/19] refactor(gnovm): ask assertValidAssignLhs for blank range key too, like AssignStmt --- gnovm/pkg/gnolang/type_check.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/gnovm/pkg/gnolang/type_check.go b/gnovm/pkg/gnolang/type_check.go index f84f80754c7..d90c0dcfd36 100644 --- a/gnovm/pkg/gnolang/type_check.go +++ b/gnovm/pkg/gnolang/type_check.go @@ -839,11 +839,12 @@ func (x *RangeStmt) AssertCompatible(store Store, last BlockNode) { if x.Op != ASSIGN { return } - // A blank or absent operand has no static type and nothing to check; - // handle each operand independently, like AssignStmt.AssertCompatible. + // Validity is asked of every operand (blank is a valid target); only + // the type check is skipped for a blank operand, which has no static + // type — per-operand, like AssignStmt.AssertCompatible. var kt, vt Type + assertValidAssignLhs(store, last, x.Key) if !isBlankIdentifier(x.Key) { - assertValidAssignLhs(store, last, x.Key) kt = evalStaticTypeOf(store, last, x.Key) } if x.Value != nil && !isBlankIdentifier(x.Value) { From c8ff60ed9b4ced8c809628bcc9fdbd78d3c98073 Mon Sep 17 00:00:00 2001 From: ltzMaxwell Date: Thu, 11 Jun 2026 18:50:10 +0800 Subject: [PATCH 19/19] refactor(gnovm): ask assertValidAssignLhs for blank range value too --- gnovm/pkg/gnolang/type_check.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gnovm/pkg/gnolang/type_check.go b/gnovm/pkg/gnolang/type_check.go index b5008d4d918..9dc76caea71 100644 --- a/gnovm/pkg/gnolang/type_check.go +++ b/gnovm/pkg/gnolang/type_check.go @@ -847,9 +847,11 @@ func (x *RangeStmt) AssertCompatible(store Store, last BlockNode) { if !isBlankIdentifier(x.Key) { kt = evalStaticTypeOf(store, last, x.Key) } - if x.Value != nil && !isBlankIdentifier(x.Value) { + if x.Value != nil { assertValidAssignLhs(store, last, x.Value) - vt = evalStaticTypeOf(store, last, x.Value) + if !isBlankIdentifier(x.Value) { + vt = evalStaticTypeOf(store, last, x.Value) + } } if kt == nil && vt == nil { return