Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Terminating analysis #1750

Merged
merged 17 commits into from
Apr 8, 2024
10 changes: 9 additions & 1 deletion gnovm/pkg/gnolang/preprocess.go
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,15 @@ func Preprocess(store Store, ctx BlockNode, n Node) Node {
last.Define(Name(rn), anyValue(rf.Type))
}
}

// functions that don't return a value do not need termination analysis
// functions that are externally defined or builtin implemented in the vm can't be analysed
if len(ft.Results) > 0 && lastpn.PkgPath != uversePkgPath && n.Body != nil {
sa := NewStaticAnalysis()
errs := sa.Analyse(n)
if len(errs) > 0 {
panic(fmt.Sprintf("%+v\n", errs))
}
KemalBekir marked this conversation as resolved.
Show resolved Hide resolved
}
// TRANS_BLOCK -----------------------
case *FileNode:
// only for imports.
Expand Down
268 changes: 268 additions & 0 deletions gnovm/pkg/gnolang/static_analysis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
package gnolang

import (
"errors"
"fmt"
)

type StaticAnalysis struct {
KemalBekir marked this conversation as resolved.
Show resolved Hide resolved
// contexts for switch and for
contexts []Context
// funcContexts for functions and lambdas
funcContexts []FuncContext

// here we accumulate errors
// from lambdas defined in the function declaration
errs []error
}

func NewStaticAnalysis() *StaticAnalysis {
return &StaticAnalysis{contexts: make([]Context, 0), funcContexts: make([]FuncContext, 0), errs: make([]error, 0)}
}

func (s *StaticAnalysis) pushContext(ctx Context) {
s.contexts = append(s.contexts, ctx)
}

KemalBekir marked this conversation as resolved.
Show resolved Hide resolved
func (s *StaticAnalysis) popContext() Context {
last := s.contexts[len(s.contexts)-1]
s.contexts = s.contexts[0 : len(s.contexts)-1]
return last
}

func (s *StaticAnalysis) pushFuncContext(fc FuncContext) {
s.funcContexts = append(s.funcContexts, fc)
}

func (s *StaticAnalysis) popFuncContext() FuncContext {
last := s.funcContexts[len(s.funcContexts)-1]
s.funcContexts = s.funcContexts[0 : len(s.funcContexts)-1]
return last
}
KemalBekir marked this conversation as resolved.
Show resolved Hide resolved

// findCtxByLabel returns the last context if the label is empty
// otherwise it returns the context that matches the label
// if it doesn't exist, it returns nil
func (s *StaticAnalysis) findCtxByLabel(label string) Context {
if len(label) == 0 {
return s.contexts[len(s.contexts)-1]
}

for i := len(s.contexts) - 1; i > -1; i-- {
if s.contexts[i].label() == label {
return s.contexts[i]
}
}

return nil
}

func (s *StaticAnalysis) Analyse(f *FuncDecl) []error {
petar-dambovaliev marked this conversation as resolved.
Show resolved Hide resolved
s.pushFuncContext(&FuncDeclContext{
hasRet: false,
f: f,
})
term := s.staticAnalysisBlockStmt(f.Body)

// todo use later maybe?
_ = s.popFuncContext().(*FuncDeclContext)

errs := make([]error, 0)
zivkovicmilos marked this conversation as resolved.
Show resolved Hide resolved
if !term {
errs = append(errs, errors.New(fmt.Sprintf("function %q does not terminate", f.Name)))
}

errs = append(errs, s.errs...)

return errs
}

func (s *StaticAnalysis) staticAnalysisBlockStmt(stmts []Stmt) bool {
if len(stmts) == 0 {
return false
}
return s.staticAnalysisStmt(stmts[len(stmts)-1])
}

func (s *StaticAnalysis) staticAnalysisExpr(expr Expr) (bool, bool) {
KemalBekir marked this conversation as resolved.
Show resolved Hide resolved
switch n := expr.(type) {
case *CallExpr:
for _, arg := range n.Args {
term, is := s.staticAnalysisExpr(arg)

if is && !term {
return true, true
KemalBekir marked this conversation as resolved.
Show resolved Hide resolved
}
}
case *FuncLitExpr:
s.pushFuncContext(&FuncLitContext{
hasRet: false,
f: n,
})
term := s.staticAnalysisBlockStmt(n.Body)
ctx := s.popFuncContext().(*FuncLitContext)
KemalBekir marked this conversation as resolved.
Show resolved Hide resolved

KemalBekir marked this conversation as resolved.
Show resolved Hide resolved
if !term {
s.errs = append(s.errs, errors.New(fmt.Sprintf("lambda at %v does not terminate\n", ctx.f.Loc)))
}
return false, false
case *NameExpr:
return false, false
}
return false, false
}

// staticAnalysisStmt returns a boolean value,
// indicating weather a statement is terminating or not
func (s *StaticAnalysis) staticAnalysisStmt(stmt Stmt) bool {
switch n := stmt.(type) {
case *BranchStmt:
switch n.Op {
case BREAK:
ctx := s.findCtxByLabel(string(n.Label))
ctx.pushBreak(n)
case CONTINUE:
//
case DEFAULT:
//
KemalBekir marked this conversation as resolved.
Show resolved Hide resolved
case FALLTHROUGH:
return true
}
case *IfStmt:
terminates := s.staticAnalysisBlockStmt(n.Then.Body)

var elseTerminates bool
if len(n.Else.Body) > 0 {
elseTerminates = s.staticAnalysisBlockStmt(n.Else.Body)
}

return terminates && elseTerminates
case *ForStmt:
s.pushContext(&ForContext{forstmt: n})
_ = s.staticAnalysisBlockStmt(n.Body)

ctx := s.popContext().(*ForContext)

// there are no "break" statements referring to the "for" statement, and
hasNoBreaks := len(ctx.breakstmts) == 0
// the loop condition is absent, and
hasNoCond := n.Cond == nil

// the "for" statement does not use a range clause.
// this one is always false because in our nodes
// the range loop is a different data structure
hasRange := false

terminates := hasNoBreaks && hasNoCond && !hasRange

if !terminates {
return false
}

return true
KemalBekir marked this conversation as resolved.
Show resolved Hide resolved
// for statement
case *ReturnStmt:
// n.Results
KemalBekir marked this conversation as resolved.
Show resolved Hide resolved
return true
case *AssignStmt:
for _, rh := range n.Rhs {
term, is := s.staticAnalysisExpr(rh)

if is && !term {
return true
}
}
return false
case *SwitchStmt:
// there is a default case, and
var hasDefault bool
for _, clause := range n.Clauses {
// nil case means default
if clause.Cases == nil {
hasDefault = true
break
}
}

s.pushContext(&SwitchContext{switchStmt: n})

// the statement lists in each case,
// including the default
// end in a terminating statement,
// or a possibly labeled "fallthrough" statement.
casesTerm := true

for _, clause := range n.Clauses {
ct := s.staticAnalysisBlockStmt(clause.Body)
casesTerm = casesTerm && ct
KemalBekir marked this conversation as resolved.
Show resolved Hide resolved
}

ctx := s.popContext().(*SwitchContext)
// there are no "break" statements referring to the "switch" statement
hasNoBreaks := len(ctx.breakstmts) == 0

terminates := hasNoBreaks && hasDefault && casesTerm

if !terminates {
return false
}

return true
KemalBekir marked this conversation as resolved.
Show resolved Hide resolved
case *PanicStmt:
return true
}
return false
}

type FuncContext interface {
isLastExprRet() bool
}

type FuncLitContext struct {
hasRet bool
f *FuncLitExpr
}

func (fdc *FuncLitContext) isLastExprRet() bool {
return fdc.hasRet
}

type FuncDeclContext struct {
hasRet bool
f *FuncDecl
}

func (fdc *FuncDeclContext) isLastExprRet() bool {
KemalBekir marked this conversation as resolved.
Show resolved Hide resolved
return fdc.hasRet
}

type Context interface {
KemalBekir marked this conversation as resolved.
Show resolved Hide resolved
label() string
pushBreak(breakstmt *BranchStmt)
}
KemalBekir marked this conversation as resolved.
Show resolved Hide resolved
KemalBekir marked this conversation as resolved.
Show resolved Hide resolved

KemalBekir marked this conversation as resolved.
Show resolved Hide resolved
type ForContext struct {
forstmt *ForStmt
breakstmts []*BranchStmt
}

func (fc *ForContext) label() string {
return string(fc.forstmt.Label)
}

func (fc *ForContext) pushBreak(breakstmt *BranchStmt) {
fc.breakstmts = append(fc.breakstmts, breakstmt)
}

type SwitchContext struct {
switchStmt *SwitchStmt
breakstmts []*BranchStmt
}

func (sc *SwitchContext) label() string {
return string(sc.switchStmt.Label)
}

func (sc *SwitchContext) pushBreak(breakstmt *BranchStmt) {
sc.breakstmts = append(sc.breakstmts, breakstmt)
}
Loading
Loading