Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
cd62122
test(gnovm): add range_blank_key filetest for nil-key range assertion
ltzmaxwell Jun 1, 2026
72007ce
fix(gnovm): guard nil kt in assertIndexTypeIsInt for blank-key range
ltzmaxwell Jun 1, 2026
fa5c11d
chore(gnovm): tighten range_blank_key fix and filetest
ltzmaxwell Jun 1, 2026
5ddcfbb
Merge remote-tracking branch 'origin/master' into fix/range_blank_key
ltzmaxwell Jun 10, 2026
5e72fed
fix(gnovm): handle blank range key/value per-operand in AssertCompatible
ltzmaxwell Jun 10, 2026
c15e60b
refactor(gnovm): rely on blank-type convention in map range check, fl…
ltzmaxwell Jun 10, 2026
fadc5fa
refactor(gnovm): drop redundant vt nil guards in range type checks
ltzmaxwell Jun 10, 2026
af1d4ba
refactor(gnovm): hoist blank-target nil case to top of checkAssignableTo
ltzmaxwell Jun 10, 2026
20da0c7
test(gnovm): add assign_range_f/g filetests (const as range value ope…
ltzmaxwell Jun 11, 2026
f2e16f1
fix(gnovm): reject unassignable range value operand in AssertCompatible
ltzmaxwell Jun 11, 2026
b77da58
test(gnovm): add assign_op_a/b and incdec_a5 filetests (unassignable …
ltzmaxwell Jun 11, 2026
f756434
fix(gnovm): validate assign target for compound assign and inc/dec st…
ltzmaxwell Jun 11, 2026
d56157a
test(gnovm): add assign_nil3 filetest (_ = nil must be rejected)
ltzmaxwell Jun 11, 2026
6c58960
fix(gnovm): reject bare untyped nil assigned to blank identifier
ltzmaxwell Jun 11, 2026
7c99b14
refactor(gnovm): require non-nil dt in checkAssignableTo, skip blank …
ltzmaxwell Jun 11, 2026
e331865
test(gnovm): add assign_range_h filetest (nil as range value operand)
ltzmaxwell Jun 11, 2026
dc75a8a
docs(gnovm): explain nil-dt ambiguity on checkAssignableTo panic
ltzmaxwell Jun 11, 2026
8e4f7bc
Merge branch 'fix/range_blank_key' into fix/assign_range_f
ltzmaxwell Jun 11, 2026
764d0b8
refactor(gnovm): panic explicitly on nil kt in assertIndexTypeIsInt
ltzmaxwell Jun 11, 2026
61c7d56
Merge branch 'fix/range_blank_key' into fix/assign_range_f
ltzmaxwell Jun 11, 2026
935a2dc
refactor(gnovm): ask assertValidAssignLhs for blank range key too, li…
ltzmaxwell Jun 11, 2026
b3e89b1
Merge branch 'fix/range_blank_key' into fix/assign_range_f
ltzmaxwell Jun 11, 2026
c8ff60e
refactor(gnovm): ask assertValidAssignLhs for blank range value too
ltzmaxwell Jun 11, 2026
2b86b9f
refactor(gnovm): restructure range checks per-operand instead of per-…
ltzmaxwell Jun 11, 2026
bcf2d9f
Merge branch 'fix/assign_range_f' into fix/range_blank_key
ltzmaxwell Jun 11, 2026
87c1804
refactor(gnovm): extract evalAssignLhsType helper for validity+type o…
ltzmaxwell Jun 11, 2026
e7347e3
refactor(gnovm): use evalAssignLhsType in remaining AssignStmt branches
ltzmaxwell Jun 11, 2026
1e7d7df
test(gnovm): add assign_op_c filetest (blank as compound-assign target)
ltzmaxwell Jun 11, 2026
248bd40
fix(gnovm): reject blank identifier as compound-assign target with pr…
ltzmaxwell Jun 11, 2026
e7a1197
refactor(gnovm): give IncDecStmt.AssertCompatible the assigning-state…
ltzmaxwell Jun 12, 2026
542a673
test(gnovm): add assign_op_d/e/f filetests (invalid compound-assign RHS)
ltzmaxwell Jun 12, 2026
0af1981
fix(gnovm): run assertValidAssignRhs for compound assignments too
ltzmaxwell Jun 12, 2026
c654faf
test(gnovm): add filetests for range/index ,ok over declared map types
ltzmaxwell Jun 12, 2026
693898f
fix(gnovm): use baseOf in range and map-index ,ok operand checks for …
ltzmaxwell Jun 12, 2026
e1b2677
test(gnovm): add assign_range_j (named int range key); sync c/d goldens
ltzmaxwell Jun 12, 2026
eddaff3
fix(gnovm): check range index by assignability to int, not kind
ltzmaxwell Jun 12, 2026
e7ce39d
test(gnovm): add assign_range_k (named rune range value); sync e golden
ltzmaxwell Jun 12, 2026
448ff33
fix(gnovm): check string range value by assignability to int32, not kind
ltzmaxwell Jun 12, 2026
17ab57c
test(gnovm): add assign_op_g order pin (invalid RHS wins over unassig…
ltzmaxwell Jun 12, 2026
96871b4
refactor(gnovm): fold identical slice/array value case in range check
ltzmaxwell Jun 12, 2026
0c99046
test(gnovm): pin define from nil interface conversion (assign_nil4)
ltzmaxwell Jun 12, 2026
5d5a459
docs(gnovm): clarify blank-nil rejection comment in assertValidAssignRhs
ltzmaxwell Jun 12, 2026
0509a19
refactor(gnovm): only folded ConstExpr counts as bare nil; panic on u…
ltzmaxwell Jun 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions gnovm/pkg/gnolang/preprocess.go
Original file line number Diff line number Diff line change
Expand Up @@ -2876,8 +2876,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node {

// TRANS_LEAVE -----------------------
case *IncDecStmt:
xt := evalStaticTypeOf(store, last, n.X)
n.AssertCompatible(xt)
n.AssertCompatible(store, last)

// TRANS_LEAVE -----------------------
case *ForStmt:
Expand Down Expand Up @@ -4715,8 +4714,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 {
Expand All @@ -4733,7 +4734,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
Expand Down Expand Up @@ -4780,7 +4781,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)
Expand Down
169 changes: 103 additions & 66 deletions gnovm/pkg/gnolang/type_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -385,9 +385,18 @@ func checkAssignableTo(n Node, xt, dt Type) (err error) {
if debug {
debug.Printf("checkAssignableTo, xt: %v dt: %v \n", xt, dt)
}
// 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. 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)")
}
// 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) {
Expand All @@ -407,8 +416,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
Expand Down Expand Up @@ -808,7 +815,8 @@ func (x *UnaryExpr) AssertCompatible(t Type) {
}
}

func (x *IncDecStmt) AssertCompatible(t Type) {
func (x *IncDecStmt) AssertCompatible(store Store, last BlockNode) {
t := evalStaticTypeOf(store, last, x.X)
// check compatible
if checker, ok := IncDecStmtChecker[x.Op]; ok {
if !checker(t) {
Expand All @@ -817,57 +825,48 @@ func (x *IncDecStmt) AssertCompatible(t Type) {
} else {
panic(fmt.Sprintf("checker for %s does not exist", x.Op))
}
}

func assertIndexTypeIsInt(kt Type) {
if kt.Kind() != IntKind {
panic(fmt.Sprintf("index type should be int, but got %v", kt))
}
// like go/types, operator/type errors take precedence over
// target assignability.
assertValidAssignLhs(store, last, x.X)
}

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
}
assertValidAssignLhs(store, last, x.Key)
// if is valid left value

kt := evalStaticTypeOf(store, last, x.Key)
// Per-operand, like AssignStmt.AssertCompatible: kt/vt are nil iff the
// operand is blank, and the type checks below are skipped for it.
kt := evalAssignLhsType(store, last, x.Key)
var vt Type
if x.Value != nil {
vt = evalStaticTypeOf(store, last, x.Value)
vt = evalAssignLhsType(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 vt != nil {
mustAssignableTo(x, cxt.Value, vt)
}
case *SliceType:
assertIndexTypeIsInt(kt)
if vt != nil {
mustAssignableTo(x, cxt.Elt, vt)
}
case *ArrayType:
assertIndexTypeIsInt(kt)
if vt != nil {
mustAssignableTo(x, cxt.Elt, vt)
}
case PrimitiveType:
if cxt.Kind() == StringKind {
if kt != nil && kt.Kind() != IntKind {
panic(fmt.Sprintf("index type should be int, but got %v", kt))
xt := baseOf(evalStaticTypeOf(store, last, x.X))
if kt != nil {
switch cxt := xt.(type) {
case *MapType:
mustAssignableTo(x, cxt.Key, kt)
case *SliceType, *ArrayType:
mustAssignableTo(x, IntType, kt)
case PrimitiveType:
if cxt.Kind() == StringKind {
mustAssignableTo(x, IntType, kt)
}
if vt != nil {
if vt.Kind() != Int32Kind { // rune
panic(fmt.Sprintf("value type should be int32, but got %v", kt))
}
}
}
if vt != nil {
switch cxt := xt.(type) {
case *MapType:
mustAssignableTo(x, cxt.Value, vt)
case *SliceType, *ArrayType:
mustAssignableTo(x, cxt.Elem(), vt)
case PrimitiveType:
if cxt.Kind() == StringKind {
mustAssignableTo(x, Int32Type, vt) // rune
}
}
}
Expand All @@ -894,9 +893,7 @@ func (x *AssignStmt) AssertCompatible(store Store, last BlockNode) {
if x.Op == ASSIGN {
// check assignable
for i, lx := range x.Lhs {
assertValidAssignLhs(store, last, lx)
if !isBlankIdentifier(lx) {
lxt := evalStaticTypeOf(store, last, lx)
if lxt := evalAssignLhsType(store, last, lx); lxt != nil {
mustAssignableTo(x, cft.Results[i].Type, lxt)
}
}
Expand All @@ -908,16 +905,12 @@ func (x *AssignStmt) AssertCompatible(store Store, last BlockNode) {
}
if x.Op == ASSIGN {
// check first value
assertValidAssignLhs(store, last, x.Lhs[0])
if !isBlankIdentifier(x.Lhs[0]) { // see composite3.gno
dt := evalStaticTypeOf(store, last, x.Lhs[0])
if dt := evalAssignLhsType(store, last, x.Lhs[0]); dt != nil { // see composite3.gno
ift := evalStaticTypeOf(store, last, cx)
mustAssignableTo(x, ift, dt)
}
// check second value
assertValidAssignLhs(store, last, x.Lhs[1])
if !isBlankIdentifier(x.Lhs[1]) { // see composite3.gno
dt := evalStaticTypeOf(store, last, x.Lhs[1])
if dt := evalAssignLhsType(store, last, x.Lhs[1]); dt != nil { // see composite3.gno
if dt.Kind() != BoolKind { // typed, not bool
panic(fmt.Sprintf("want bool type got %v", dt))
}
Expand All @@ -929,16 +922,14 @@ func (x *AssignStmt) AssertCompatible(store Store, last BlockNode) {
panic("should not happen")
}
if x.Op == ASSIGN {
assertValidAssignLhs(store, last, x.Lhs[0])
if !isBlankIdentifier(x.Lhs[0]) {
lt := evalStaticTypeOf(store, last, x.Lhs[0])
if lt := evalAssignLhsType(store, last, x.Lhs[0]); lt != nil {
if _, ok := cx.X.(*NameExpr); ok {
rt := evalStaticTypeOf(store, last, cx.X)
rt := baseOf(evalStaticTypeOf(store, last, cx.X))
if mt, ok := rt.(*MapType); ok {
mustAssignableTo(x, mt.Value, lt)
}
} else if _, ok := cx.X.(*CompositeLitExpr); ok {
cpt := evalStaticTypeOf(store, last, cx.X)
cpt := baseOf(evalStaticTypeOf(store, last, cx.X))
if mt, ok := cpt.(*MapType); ok {
mustAssignableTo(x, mt.Value, lt)
} else {
Expand All @@ -947,10 +938,8 @@ func (x *AssignStmt) AssertCompatible(store Store, last BlockNode) {
}
}

assertValidAssignLhs(store, last, x.Lhs[1])
if !isBlankIdentifier(x.Lhs[1]) {
dt := evalStaticTypeOf(store, last, x.Lhs[1])
if dt != nil && dt.Kind() != BoolKind { // typed, not bool
if dt := evalAssignLhsType(store, last, x.Lhs[1]); dt != nil {
if dt.Kind() != BoolKind { // typed, not bool
panic(fmt.Sprintf("want bool type got %v", dt))
}
}
Expand All @@ -963,8 +952,10 @@ func (x *AssignStmt) AssertCompatible(store Store, last BlockNode) {
if x.Op == ASSIGN {
// assert valid left value
for i, lx := range x.Lhs {
assertValidAssignLhs(store, last, lx)
lt := evalStaticTypeOf(store, last, lx)
lt := evalAssignLhsType(store, last, lx)
if lt == nil {
continue // blank: nothing to check
}
rt := evalStaticTypeOf(store, last, x.Rhs[i])
mustAssignableTo(x, rt, lt)
}
Expand All @@ -977,6 +968,12 @@ func (x *AssignStmt) AssertCompatible(store Store, last BlockNode) {
panic("assignment operator " + x.Op.TokenString() +
" requires only one expression on lhs and rhs")
}
// `_ += x` reads the target (it desugars to `_ = _ + x`), unlike
// plain assignment where blank is a valid (discarded) target.
if isBlankIdentifier(x.Lhs[0]) {
panic("cannot use _ as value or type")
}
assertValidAssignRhs(store, last, x)
lt := evalStaticTypeOf(store, last, x.Lhs[0])
rt := evalStaticTypeOf(store, last, x.Rhs[0])

Expand Down Expand Up @@ -1012,6 +1009,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])
}
}

Expand Down Expand Up @@ -1041,6 +1041,17 @@ func assertValidAssignLhs(store Store, last BlockNode, lx Expr) {
}
}

// evalAssignLhsType asserts that lx is a valid assignment target and returns
// its static type, or nil iff lx is blank: blank is a valid target but has no
// static type, so callers skip type checks on a nil result.
func evalAssignLhsType(store Store, last BlockNode, lx Expr) Type {
assertValidAssignLhs(store, last, lx)
if isBlankIdentifier(lx) {
return nil
}
return evalStaticTypeOf(store, last, lx)
}

func assertValidAssignRhs(store Store, last BlockNode, n Node) {
var exps []Expr
switch x := n.(type) {
Expand All @@ -1052,7 +1063,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) {
Expand All @@ -1063,6 +1074,13 @@ func assertValidAssignRhs(store Store, last BlockNode, n Node) {
panic("use of untyped nil in variable declaration")
case *AssignStmt:
if x.Op != DEFINE {
// Assigning nil to a typed target is judged later by
// the assignability check against that target's type.
// A blank target has no type and no later check, so
// `_ = nil` must be rejected here.
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")
Expand Down Expand Up @@ -1163,6 +1181,25 @@ func isBlankIdentifier(x Expr) bool {
return false
}

// isNilIdentifier reports whether x is the bare `nil` keyword. By check time
// `nil` is always const-folded, so the original syntax is recovered from the
// ConstExpr's Source; a conversion like `T(nil)` also has a nil static type
// but a non-NameExpr Source, and reports false.
func isNilIdentifier(x Expr) bool {
switch cx := x.(type) {
case *ConstExpr:
nx, ok := cx.Source.(*NameExpr)
return ok && nx.Name == "nil"
case *NameExpr:
if cx.Name == "nil" {
panic("should not happen: unfolded nil NameExpr in isNilIdentifier")
}
return false
default:
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 {
Expand Down
24 changes: 18 additions & 6 deletions gnovm/pkg/gnolang/type_check_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,26 @@ 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",
xt: nil,
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",
Expand All @@ -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 {
Expand Down
Loading