Skip to content

Commit

Permalink
fix(gnovm): prevent cyclic references in type declarations (#2081)
Browse files Browse the repository at this point in the history
Address #2008.


In this pull request, we're implementing a special handling for type
declarations to check cynic dependency. Due to their unique nature, we
must meticulously search through their fields to identify potential
cyclic reference.

<details><summary>Contributors' checklist...</summary>

- [*] Added new tests, or not needed, or not feasible
- [ ] Provided an example (e.g. screenshot) to aid review or the PR is
self-explanatory
- [ ] Updated the official documentation or not needed
- [*] No breaking changes were made, or a `BREAKING CHANGE: xxx` message
was included in the description
- [ ] Added references to related issues and PRs
- [ ] Provided any useful hints for running manual tests
- [ ] Added new benchmarks to [generated
graphs](https://gnoland.github.io/benchmarks), if any. More info
[here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
</details>

---------

Co-authored-by: deelawn <[email protected]>
  • Loading branch information
ltzmaxwell and deelawn authored Aug 16, 2024
1 parent 6f7f589 commit 1e0e52c
Show file tree
Hide file tree
Showing 28 changed files with 474 additions and 7 deletions.
109 changes: 102 additions & 7 deletions gnovm/pkg/gnolang/preprocess.go
Original file line number Diff line number Diff line change
Expand Up @@ -2906,6 +2906,92 @@ func convertConst(store Store, last BlockNode, cx *ConstExpr, t Type) {
}
}

func assertTypeDeclNoCycle(store Store, last BlockNode, td *TypeDecl, stack *[]Name) {
assertTypeDeclNoCycle2(store, last, td.Type, stack, false, td.IsAlias)
}

func assertTypeDeclNoCycle2(store Store, last BlockNode, x Expr, stack *[]Name, indirect bool, isAlias bool) {
if x == nil {
panic("unexpected nil expression when checking for type declaration cycles")
}

var lastX Expr
defer func() {
if _, ok := lastX.(*NameExpr); ok {
// pop stack
*stack = (*stack)[:len(*stack)-1]
}
}()

switch cx := x.(type) {
case *NameExpr:
var msg string

// Function to build the error message
buildMessage := func() string {
for j := 0; j < len(*stack); j++ {
msg += fmt.Sprintf("%s -> ", (*stack)[j])
}
return msg + string(cx.Name) // Append the current name last
}

// Check for existence of cx.Name in stack
findCycle := func() {
for _, n := range *stack {
if n == cx.Name {
msg = buildMessage()
panic(fmt.Sprintf("invalid recursive type: %s", msg))
}
}
}

if indirect && !isAlias {
*stack = (*stack)[:0]
} else {
findCycle()
*stack = append(*stack, cx.Name)
lastX = cx
}

return
case *SelectorExpr:
assertTypeDeclNoCycle2(store, last, cx.X, stack, indirect, isAlias)
case *StarExpr:
assertTypeDeclNoCycle2(store, last, cx.X, stack, true, isAlias)
case *FieldTypeExpr:
assertTypeDeclNoCycle2(store, last, cx.Type, stack, indirect, isAlias)
case *ArrayTypeExpr:
if cx.Len != nil {
assertTypeDeclNoCycle2(store, last, cx.Len, stack, indirect, isAlias)
}
assertTypeDeclNoCycle2(store, last, cx.Elt, stack, indirect, isAlias)
case *SliceTypeExpr:
assertTypeDeclNoCycle2(store, last, cx.Elt, stack, true, isAlias)
case *InterfaceTypeExpr:
for i := range cx.Methods {
assertTypeDeclNoCycle2(store, last, &cx.Methods[i], stack, indirect, isAlias)
}
case *ChanTypeExpr:
assertTypeDeclNoCycle2(store, last, cx.Value, stack, true, isAlias)
case *FuncTypeExpr:
for i := range cx.Params {
assertTypeDeclNoCycle2(store, last, &cx.Params[i], stack, true, isAlias)
}
for i := range cx.Results {
assertTypeDeclNoCycle2(store, last, &cx.Results[i], stack, true, isAlias)
}
case *MapTypeExpr:
assertTypeDeclNoCycle2(store, last, cx.Key, stack, true, isAlias)
assertTypeDeclNoCycle2(store, last, cx.Value, stack, true, isAlias)
case *StructTypeExpr:
for i := range cx.Fields {
assertTypeDeclNoCycle2(store, last, &cx.Fields[i], stack, indirect, isAlias)
}
default:
}
return
}

// Returns any names not yet defined nor predefined in expr. These happen
// upon transcribe:enter from the top, so value paths cannot be used. If no
// names are un and x is TypeExpr, evalStaticType(store,last, x) must not
Expand Down Expand Up @@ -3197,27 +3283,36 @@ func predefineNow(store Store, last BlockNode, d Decl) (Decl, bool) {
}
}
}()
m := make(map[Name]struct{})
return predefineNow2(store, last, d, m)
stack := &[]Name{}
return predefineNow2(store, last, d, stack)
}

func predefineNow2(store Store, last BlockNode, d Decl, m map[Name]struct{}) (Decl, bool) {
func predefineNow2(store Store, last BlockNode, d Decl, stack *[]Name) (Decl, bool) {
pkg := packageOf(last)
// pre-register d.GetName() to detect circular definition.
for _, dn := range d.GetDeclNames() {
if isUverseName(dn) {
panic(fmt.Sprintf(
"builtin identifiers cannot be shadowed: %s", dn))
}
m[dn] = struct{}{}
*stack = append(*stack, dn)
}

// check type decl cycle
if td, ok := d.(*TypeDecl); ok {
// recursively check
assertTypeDeclNoCycle(store, last, td, stack)
}

// recursively predefine dependencies.
for {
un := tryPredefine(store, last, d)
if un != "" {
// check circularity.
if _, ok := m[un]; ok {
panic(fmt.Sprintf("constant definition loop with %s", un))
for _, n := range *stack {
if n == un {
panic(fmt.Sprintf("constant definition loop with %s", un))
}
}
// look up dependency declaration from fileset.
file, decl := pkg.FileSet.GetDeclFor(un)
Expand All @@ -3226,7 +3321,7 @@ func predefineNow2(store Store, last BlockNode, d Decl, m map[Name]struct{}) (De
panic("all types from files in file-set should have already been predefined")
}
// predefine dependency (recursive).
*decl, _ = predefineNow2(store, file, *decl, m)
*decl, _ = predefineNow2(store, file, *decl, stack)
} else {
break
}
Expand Down
10 changes: 10 additions & 0 deletions gnovm/tests/files/circular_constant.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package main

const A = B
const B = A + 1

func main() {
}

// Error:
// main/files/circular_constant.gno:3:7: constant definition loop with A
13 changes: 13 additions & 0 deletions gnovm/tests/files/recursive1.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package main

type S struct {
T S
}

func main() {
var a, b S
println(a == b)
}

// Error:
// main/files/recursive1.gno:1:1: invalid recursive type: S -> S
15 changes: 15 additions & 0 deletions gnovm/tests/files/recursive1a.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package main

type S1 *S

type S struct {
T S1
}

func main() {
var a, b S
println(a == b)
}

// Output:
// true
16 changes: 16 additions & 0 deletions gnovm/tests/files/recursive1b.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package main

type S struct {
T *S
B Integer
}

type Integer int

func main() {
var a, b S
println(a == b)
}

// Output:
// true
17 changes: 17 additions & 0 deletions gnovm/tests/files/recursive1c.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package main

import "fmt"

type S struct {
A [2][2]S
}

func main() {
var a, b S

fmt.Println(a)
fmt.Println(b)
}

// Error:
// main/files/recursive1c.gno:1:1: invalid recursive type: S -> S
17 changes: 17 additions & 0 deletions gnovm/tests/files/recursive1d.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package main

import "fmt"

type S struct {
A [2]S
}

func main() {
var a, b S

fmt.Println(a)
fmt.Println(b)
}

// Error:
// main/files/recursive1d.gno:1:1: invalid recursive type: S -> S
13 changes: 13 additions & 0 deletions gnovm/tests/files/recursive1e.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package main

type S struct {
A [2][]S
}

func main() {
var a, b S
println(a)
}

// Output:
// (struct{(array[(nil []main.S),(nil []main.S)] [2][]main.S)} main.S)
13 changes: 13 additions & 0 deletions gnovm/tests/files/recursive1f.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package main

func main() {
type S struct {
T S
}

var a, b S
println(a == b)
}

// Error:
// main/files/recursive1f.gno:3:1: invalid recursive type: S -> S
21 changes: 21 additions & 0 deletions gnovm/tests/files/recursive2.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package main

type A struct {
X B
}

type B struct {
X C
}

type C struct {
X A
}

func main() {
var p, q A
println(p == q)
}

// Error:
// main/files/recursive2.gno:1:1: invalid recursive type: A -> B -> C -> A
21 changes: 21 additions & 0 deletions gnovm/tests/files/recursive2a.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package main

type A struct {
X B
}

type B struct {
X int
}

type C struct {
X A
}

func main() {
var p, q A
println(p == q)
}

// Output:
// true
21 changes: 21 additions & 0 deletions gnovm/tests/files/recursive2b.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package main

type A struct {
X B
}

type B struct {
X C
}

type C struct {
X *A
}

func main() {
var p, q A
println(p == q)
}

// Output:
// true
21 changes: 21 additions & 0 deletions gnovm/tests/files/recursive2c.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package main

func main() {
type A struct {
X B
}

type B struct {
X C
}

type C struct {
X A
}

var p, q A
println(p == q)
}

// Error:
// main/files/recursive2c.gno:3:1: name B not defined in fileset with files [files/recursive2c.gno]
21 changes: 21 additions & 0 deletions gnovm/tests/files/recursive2d.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package main

type A struct {
X *B
}

type B struct {
X int
}

type C struct {
X A
}

func main() {
var p, q A
println(p == q)
}

// Output:
// true
13 changes: 13 additions & 0 deletions gnovm/tests/files/recursive3.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package main

type S struct {
T *S
}

func main() {
var a, b S
println(a == b)
}

// Output:
// true
Loading

0 comments on commit 1e0e52c

Please sign in to comment.