~mna/snow

8c260458e1791effbd79181ef42debb085eb5c91 — Martin Angers 1 year, 11 months ago 2333bae
pkg/typecheck: add query AST support to check if guard falls through
29 files changed, 435 insertions(+), 1 deletions(-)

M pkg/typecheck/check_pass.go
A pkg/typecheck/query.go
A pkg/typecheck/query_test.go
M pkg/typecheck/scope_pass.go
A pkg/typecheck/testdata/check/fn_guard_fallthrough.snow.err
A pkg/typecheck/testdata/check/fn_guard_fallthrough.snow.want
A pkg/typecheck/testdata/fn_guard_fallthrough.snow
A pkg/typecheck/testdata/query/fallthrough/fn_block_return.snow
A pkg/typecheck/testdata/query/fallthrough/fn_body_true.snow
A pkg/typecheck/testdata/query/fallthrough/fn_guard_false.snow
A pkg/typecheck/testdata/query/fallthrough/fn_guard_true.snow
A pkg/typecheck/testdata/query/fallthrough/fn_if_else_false.snow
A pkg/typecheck/testdata/query/fallthrough/fn_if_else_true.snow
A pkg/typecheck/testdata/query/fallthrough/fn_if_else_true2.snow
A pkg/typecheck/testdata/query/fallthrough/fn_if_elseif_else_false.snow
A pkg/typecheck/testdata/query/fallthrough/fn_if_elseif_else_true.snow
A pkg/typecheck/testdata/query/fallthrough/fn_if_elseif_true.snow
A pkg/typecheck/testdata/query/fallthrough/fn_if_elseif_true2.snow
A pkg/typecheck/testdata/query/fallthrough/fn_if_no_else_true.snow
A pkg/typecheck/testdata/query/fallthrough/fn_nested_false.snow
A pkg/typecheck/testdata/query/fallthrough/fn_nested_inner_false.snow
A pkg/typecheck/testdata/query/fallthrough/fn_nested_true.snow
A pkg/typecheck/testdata/query/fallthrough/fn_stmt_after_return_false.snow
A pkg/typecheck/testdata/query/fallthrough/guard_if_else_false.snow
A pkg/typecheck/testdata/query/fallthrough/guard_if_true.snow
A pkg/typecheck/testdata/scopes/fn_guard_fallthrough.snow.err
A pkg/typecheck/testdata/scopes/fn_guard_fallthrough.snow.want
A pkg/typecheck/testdata/types/fn_guard_fallthrough.snow.err
A pkg/typecheck/testdata/types/fn_guard_fallthrough.snow.want
M pkg/typecheck/check_pass.go => pkg/typecheck/check_pass.go +1 -1
@@ 405,7 405,7 @@ func (c *checkPass) Visit(n ast.Node) ast.Visitor {
		}
		if n.Else != nil {
			ast.Walk(c, n.Else)
			// TODO: must not fall through the block
			// TODO: must not fall through the block (possibly in a subsequent "static analysis" pass)
		}

	case *ast.ElseClause:

A pkg/typecheck/query.go => pkg/typecheck/query.go +87 -0
@@ 0,0 1,87 @@
package typecheck

import "git.sr.ht/~mna/snow/pkg/ast"

// This file contains various functions that allow querying of the AST
// for static analysis.

// FallsThrough returns true if any execution path can fall through from the
// Block statement into the parent block. It assumes any guard statement
// contained in that block are semantically correct and don't fall through.
// It ignores any BadStmt nodes.
func FallsThrough(scope *Scope, block *ast.Block) bool {
	// Currently, the only way *not* to fall through is to return from the
	// block (no break/continue/panic/infinite loop/NoReturn fn yet).

	// NOTE: for now the scope is unnecessary, but for (possibly labeled)
	// break/continue it will be required to lookup where the execution
	// resumes (i.e. it has to be outside the guard's containing scope).

	b := blockFallthrough{true}
	ast.Walk(&b, block)
	return b.doesFallthrough
}

type blockFallthrough struct {
	doesFallthrough bool
}

func (b *blockFallthrough) Visit(n ast.Node) ast.Visitor {
	if n == nil {
		return nil
	}

	switch n := n.(type) {
	case *ast.IfStmt:
		// if it doesn't have an else clause, ignore it, even if it doesn't
		// fall through we learn nothing about the block in general.
		if n.Else == nil {
			return nil
		}

		// otherwise, if both the if and else (and all else if in between) do
		// return, then the branch doesn't fall through.
		bb1 := blockFallthrough{true}
		bb2 := blockFallthrough{true}
		ast.Walk(&bb1, n.Body)
		ast.Walk(&bb2, n.Else)
		if !bb1.doesFallthrough && !bb2.doesFallthrough {
			// both the if and the else return, so the if as a whole doesn't fall
			// through (this works recursively, if one if-elseif then has an if
			// without else in the elseif, it will return that it falls through).
			b.doesFallthrough = false
		}

	case *ast.GuardStmt:
		// assume that the guard's else clause doesn't return - it cannot by
		// the semantics of the language, and from the typecheck point of view,
		// all guard statements will be verified, so if one of them do fall through,
		// an error will be reported.
		return nil

	case *ast.ElseClause:
		return b

	case *ast.ReturnStmt:
		b.doesFallthrough = false

	case *ast.Block:
		bb := blockFallthrough{true}
		for _, stmt := range n.Stmts {
			ast.Walk(&bb, stmt)
			if !bb.doesFallthrough {
				// rest of the statements won't be executed
				b.doesFallthrough = false
				return nil
			}
		}
	}

	// ast.Expr, *ast.CommentGroup, *ast.File, *ast.FnDef, *ast.VarDef, *ast.AssignStmt,
	//*ast.ExprStmt, *ast.ExprItem, *ast.BadStmt:
	//
	// irrelevant to this analysis
	// FnDef doesn't tell us nothing about the block itself, it just
	// defines a callable symbol.
	return nil
}

A pkg/typecheck/query_test.go => pkg/typecheck/query_test.go +78 -0
@@ 0,0 1,78 @@
package typecheck

import (
	"io/ioutil"
	"path/filepath"
	"strconv"
	"strings"
	"testing"

	"git.sr.ht/~mna/snow/pkg/ast"
)

//var testUpdateQueryTests = flag.Bool("test.update-query-tests", false, "If set, replace expected typecheck query test results with actual results.")

func TestQuery_Fallthrough(t *testing.T) {
	baseDir := filepath.Join("testdata", "query", "fallthrough")
	fis, err := ioutil.ReadDir(baseDir)
	if err != nil {
		t.Fatal(err)
	}

	for _, fi := range fis {
		if !fi.Mode().IsRegular() {
			continue
		}
		if filepath.Ext(fi.Name()) != ".snow" {
			continue
		}

		t.Run(fi.Name(), func(t *testing.T) {
			unit, err := CheckFiles(ScopePass, filepath.Join(baseDir, fi.Name()))
			if err != nil {
				t.Fatal(err)
			}
			if len(unit.Files) != 1 {
				t.Fatalf("want 1 AST file, got %d", len(unit.Files))
			}
			file := unit.Files[0]
			if len(file.Comments) != 1 || len(file.Comments[0].List) != 1 {
				t.Fatal("no expected result, should have a single comment line with 'true' or 'false'")
			}
			comment := strings.TrimSpace(file.Comments[0].List[0].Text[1:])
			want, err := strconv.ParseBool(comment)
			if err != nil {
				t.Fatalf("invalid expected result, want 'true' or 'false', got %q", comment)
			}

			// validate the block that contains the symbol "here"
			var recurse func(*Scope) (*Scope, bool)
			recurse = func(s *Scope) (*Scope, bool) {
				for _, child := range s.Children {
					ss, ok := recurse(child)
					if ok {
						return ss, ok
					}
				}
				if obj := s.Lookup("here"); obj != nil {
					return s, true
				}
				return nil, false
			}

			scope, ok := recurse(unit.Universe)
			if !ok {
				t.Fatal("could not find scope with symbol 'here'")
			}
			block, ok := scope.Node.(*ast.Block)
			if !ok {
				t.Fatalf("scope containing 'here' is not a Block: %s", scope)
			}

			got := FallsThrough(scope, block)
			if got != want {
				t.Fatalf("want FallsThrough: %t, got %t", want, got)
			}
		})
	}
}

M pkg/typecheck/scope_pass.go => pkg/typecheck/scope_pass.go +10 -0
@@ 83,6 83,10 @@ func (s *scopePass) Visit(n ast.Node) ast.Visitor {
		}

	case *ast.IfStmt:
		// TODO: eventually, the if will start the scope (not the block),
		// and it will start at the block's position and the variables defined
		// in the conditions list will be in that scope. The else will also
		// start a different scope.
		if n.Body != nil {
			ast.Walk(s, n.Body)
		}


@@ 91,6 95,12 @@ func (s *scopePass) Visit(n ast.Node) ast.Visitor {
		}

	case *ast.GuardStmt:
		// TODO: unlike the IfStmt, eventually the variable declarations in
		// the conditions will be in the current scope (not in the guard/else
		// scope). But from the scope point-of-view, it should register that
		// it is the guard node that opened the scope, though it starts only
		// at the else block and does not contain any of the conditions'
		// variables.
		if n.Else != nil {
			ast.Walk(s, n.Else)
		}

A pkg/typecheck/testdata/check/fn_guard_fallthrough.snow.err => pkg/typecheck/testdata/check/fn_guard_fallthrough.snow.err +0 -0
A pkg/typecheck/testdata/check/fn_guard_fallthrough.snow.want => pkg/typecheck/testdata/check/fn_guard_fallthrough.snow.want +14 -0
@@ 0,0 1,14 @@
file [1, #0]
  fn
    ident [test] [let: () -> void]
    sig [0->0]
    block [3]
      var:
        ident [x] [var: bool]
        ident [bool] [type: bool]
      guard [1]
        item
          ident [x] [var: bool]
        else
          block [0]
      return

A pkg/typecheck/testdata/fn_guard_fallthrough.snow => pkg/typecheck/testdata/fn_guard_fallthrough.snow +6 -0
@@ 0,0 1,6 @@
fn test() {
  var x: bool
  guard x else {
  }
  return
}

A pkg/typecheck/testdata/query/fallthrough/fn_block_return.snow => pkg/typecheck/testdata/query/fallthrough/fn_block_return.snow +7 -0
@@ 0,0 1,7 @@
# false
fn test() {
  {
    var here: bool
    return
  }
}

A pkg/typecheck/testdata/query/fallthrough/fn_body_true.snow => pkg/typecheck/testdata/query/fallthrough/fn_body_true.snow +6 -0
@@ 0,0 1,6 @@
# true
fn test() {
  {
    var here: bool
  }
}

A pkg/typecheck/testdata/query/fallthrough/fn_guard_false.snow => pkg/typecheck/testdata/query/fallthrough/fn_guard_false.snow +10 -0
@@ 0,0 1,10 @@
# false
fn test() {
  {
    var here: bool
    var x : bool
    guard x else {
    }
    return
  }
}

A pkg/typecheck/testdata/query/fallthrough/fn_guard_true.snow => pkg/typecheck/testdata/query/fallthrough/fn_guard_true.snow +9 -0
@@ 0,0 1,9 @@
# true
fn test() {
  {
    var here: bool
    var x : bool
    guard x else {
    }
  }
}

A pkg/typecheck/testdata/query/fallthrough/fn_if_else_false.snow => pkg/typecheck/testdata/query/fallthrough/fn_if_else_false.snow +12 -0
@@ 0,0 1,12 @@
# false
fn test() {
  {
    var here: bool 
    var x: bool
    if x {
      return
    } else {
      return
    }
  }
}

A pkg/typecheck/testdata/query/fallthrough/fn_if_else_true.snow => pkg/typecheck/testdata/query/fallthrough/fn_if_else_true.snow +12 -0
@@ 0,0 1,12 @@
# true
fn test() {
  {
    var here: bool
    var x: bool
    if x {
      return
    } else {
      x
    }
  }
}

A pkg/typecheck/testdata/query/fallthrough/fn_if_else_true2.snow => pkg/typecheck/testdata/query/fallthrough/fn_if_else_true2.snow +12 -0
@@ 0,0 1,12 @@
# true
fn test() {
  {
    var here: bool
    var x: bool
    if x {
      x
    } else {
      return
    }
  }
}

A pkg/typecheck/testdata/query/fallthrough/fn_if_elseif_else_false.snow => pkg/typecheck/testdata/query/fallthrough/fn_if_elseif_else_false.snow +14 -0
@@ 0,0 1,14 @@
# false
fn test() {
  {
    var here: bool
    var x: bool
    if x {
      return
    } else if !x {
      return
    } else {
      return
    }
  }
}

A pkg/typecheck/testdata/query/fallthrough/fn_if_elseif_else_true.snow => pkg/typecheck/testdata/query/fallthrough/fn_if_elseif_else_true.snow +14 -0
@@ 0,0 1,14 @@
# true
fn test() {
  {
    var here: bool
    var x: bool
    if x {
      return
    } else if !x {
      x
    } else {
      return
    }
  }
}

A pkg/typecheck/testdata/query/fallthrough/fn_if_elseif_true.snow => pkg/typecheck/testdata/query/fallthrough/fn_if_elseif_true.snow +12 -0
@@ 0,0 1,12 @@
# true
fn test() {
  {
    var here: bool
    var x: bool
    if x {
      return
    } else if !x {
      return
    }
  }
}

A pkg/typecheck/testdata/query/fallthrough/fn_if_elseif_true2.snow => pkg/typecheck/testdata/query/fallthrough/fn_if_elseif_true2.snow +12 -0
@@ 0,0 1,12 @@
# true
fn test() {
  {
    var here: bool
    var x: bool
    if x {
      return
    } else if !x {
      x
    }
  }
}

A pkg/typecheck/testdata/query/fallthrough/fn_if_no_else_true.snow => pkg/typecheck/testdata/query/fallthrough/fn_if_no_else_true.snow +10 -0
@@ 0,0 1,10 @@
# true
fn test() {
  {
    var here: bool 
    var x: bool
    if x {
      return
    }
  }
}

A pkg/typecheck/testdata/query/fallthrough/fn_nested_false.snow => pkg/typecheck/testdata/query/fallthrough/fn_nested_false.snow +9 -0
@@ 0,0 1,9 @@
# false
fn test() {
  {
    {
      var here: bool
      return
    }
  }
}

A pkg/typecheck/testdata/query/fallthrough/fn_nested_inner_false.snow => pkg/typecheck/testdata/query/fallthrough/fn_nested_inner_false.snow +11 -0
@@ 0,0 1,11 @@
# false
fn test() {
  {
    {
      var here: bool
      {
        return
      }
    }
  }
}

A pkg/typecheck/testdata/query/fallthrough/fn_nested_true.snow => pkg/typecheck/testdata/query/fallthrough/fn_nested_true.snow +8 -0
@@ 0,0 1,8 @@
# true
fn test() {
  {
    {
      var here: bool
    }
  }
}

A pkg/typecheck/testdata/query/fallthrough/fn_stmt_after_return_false.snow => pkg/typecheck/testdata/query/fallthrough/fn_stmt_after_return_false.snow +8 -0
@@ 0,0 1,8 @@
# false
fn test() {
  {
    var here: bool
    return
    var x: int
  }
}

A pkg/typecheck/testdata/query/fallthrough/guard_if_else_false.snow => pkg/typecheck/testdata/query/fallthrough/guard_if_else_false.snow +17 -0
@@ 0,0 1,17 @@
# false
fn test() {
  {
    var x: bool
    guard x else {
      var here: bool
      var y: bool

      if y {
        return
      } else {
        return
      }
      var z: int
    }
  }
}

A pkg/typecheck/testdata/query/fallthrough/guard_if_true.snow => pkg/typecheck/testdata/query/fallthrough/guard_if_true.snow +14 -0
@@ 0,0 1,14 @@
# true
fn test() {
  {
    var x: bool
    guard x else {
      var here: bool
      var y: bool

      if y {
        return
      }
    }
  }
}

A pkg/typecheck/testdata/scopes/fn_guard_fallthrough.snow.err => pkg/typecheck/testdata/scopes/fn_guard_fallthrough.snow.err +0 -0
A pkg/typecheck/testdata/scopes/fn_guard_fallthrough.snow.want => pkg/typecheck/testdata/scopes/fn_guard_fallthrough.snow.want +28 -0
@@ 0,0 1,28 @@
<nil> {
.  bool
.  f32
.  f64
.  false
.  float
.  i16
.  i32
.  i64
.  i8
.  int
.  string
.  true
.  u16
.  u32
.  u64
.  u8
.  uint
.  void
.  *ast.File {
.  .  test
.  .  *ast.FnDef {
.  .  .  x
.  .  .  *ast.Block {
.  .  .  }
.  .  }
.  }
}

A pkg/typecheck/testdata/types/fn_guard_fallthrough.snow.err => pkg/typecheck/testdata/types/fn_guard_fallthrough.snow.err +0 -0
A pkg/typecheck/testdata/types/fn_guard_fallthrough.snow.want => pkg/typecheck/testdata/types/fn_guard_fallthrough.snow.want +14 -0
@@ 0,0 1,14 @@
file [1, #0]
  fn
    ident [test] [let: () -> void]
    sig [0->0]
    block [3]
      var:
        ident [x] [var: bool]
        ident [bool] [type: bool]
      guard [1]
        item
          ident [x] [var: bool]
        else
          block [0]
      return