fix: improve use of explicit nil (#96)

Type category `UnsetT` is renamed in `NilT`.
Catch invalid use of untyped nil in `:=` expression.
Convert nil to output parameter type when used in `return` expression.
Improve and add relevant unit tests.
This commit is contained in:
Marc Vertes
2019-02-20 15:44:44 +01:00
committed by Ludovic Fernandez
parent 5677e0501e
commit 32e0be8b4e
5 changed files with 91 additions and 40 deletions

View File

@@ -295,6 +295,10 @@ func (interp *Interpreter) Cfg(root *Node) ([]*Node, error) {
dest, src := n.child[0], n.lastChild()
sym, level, _ := scope.lookup(dest.ident)
if n.kind == Define {
if src.typ.cat == NilT {
err = src.cfgError("use of untyped nil")
break
}
if len(n.child) == 3 {
// type is provided in var declaration
dest.typ, err = nodeType(interp, scope, n.child[1])
@@ -320,6 +324,7 @@ func (interp *Interpreter) Cfg(root *Node) ([]*Node, error) {
// Detect invalid float truncate
if isInt(dest.typ) && isFloat(src.typ) {
err = src.cfgError("invalid float truncate")
break
}
n.findex = dest.findex
n.val = dest.val
@@ -835,6 +840,14 @@ func (interp *Interpreter) Cfg(root *Node) ([]*Node, error) {
case ReturnStmt:
wireChild(n)
n.tnext = nil
for i, c := range n.child {
if c.typ.cat == NilT {
// nil: Set node value to zero of return type
f := getAncFunc(n)
typ := f.child[2].child[1].child[i].lastChild().typ
c.val, err = typ.zero()
}
}
case SelectorExpr:
wireChild(n)
@@ -1314,6 +1327,17 @@ func setExec(n *Node) {
set(n)
}
// getAncFunc returns the first node in ancestorship which is a FuncDecl or FuncLit
func getAncFunc(n *Node) *Node {
for anc := n.anc; anc != nil; anc = anc.anc {
switch anc.kind {
case FuncDecl, FuncLit:
return anc
}
}
return nil
}
func getReturnedType(n *Node) *Type {
switch n.typ.cat {
case BuiltinT:

View File

@@ -163,7 +163,7 @@ func initUniverse() *Scope {
"iota": &Symbol{kind: Const, typ: &Type{cat: IntT}},
// predefined Go zero value
"nil": &Symbol{typ: &Type{cat: UnsetT}},
"nil": &Symbol{typ: &Type{cat: NilT}},
// predefined Go builtins
"append": &Symbol{kind: Bltn, builtin: _append},

View File

@@ -1,6 +1,7 @@
package interp_test
import (
"reflect"
"testing"
"github.com/containous/dyngo/interp"
@@ -8,15 +9,9 @@ import (
func TestEval0(t *testing.T) {
i := interp.New(interp.Opt{})
_, err := i.Eval(`var I int = 2`)
if err != nil {
t.Fatal(err)
}
evalCheck(t, i, `var I int = 2`)
t1, err := i.Eval(`I`)
if err != nil {
t.Fatal(err)
}
t1 := evalCheck(t, i, `I`)
if t1.Interface().(int) != 2 {
t.Fatalf("expected 2, got %v", t1)
}
@@ -24,19 +19,15 @@ func TestEval0(t *testing.T) {
func TestEval1(t *testing.T) {
i := interp.New(interp.Opt{})
_, err := i.Eval(`func Hello() string { return "hello" }`)
if err != nil {
t.Fatal(err)
}
evalCheck(t, i, `func Hello() string { return "hello" }`)
v := evalCheck(t, i, `Hello`)
v, err := i.Eval(`Hello`)
if err != nil {
t.Fatal(err)
}
f, ok := v.Interface().(func() string)
if !ok {
t.Fatal("conversion failed")
}
if s := f(); s != "hello" {
t.Fatalf("expected hello, got %v", s)
}
@@ -44,15 +35,9 @@ func TestEval1(t *testing.T) {
func TestEval2(t *testing.T) {
i := interp.New(interp.Opt{})
_, err := i.Eval(`package foo; var I int = 2`)
if err != nil {
t.Fatal(err)
}
evalCheck(t, i, `package foo; var I int = 2`)
t1, err := i.Eval(`foo.I`)
if err != nil {
t.Fatal(err)
}
t1 := evalCheck(t, i, `foo.I`)
if t1.Interface().(int) != 2 {
t.Fatalf("expected 2, got %v", t1)
}
@@ -60,15 +45,9 @@ func TestEval2(t *testing.T) {
func TestEval3(t *testing.T) {
i := interp.New(interp.Opt{})
_, err := i.Eval(`package foo; func Hello() string { return "hello" }`)
if err != nil {
t.Fatal(err)
}
evalCheck(t, i, `package foo; func Hello() string { return "hello" }`)
v, err := i.Eval(`foo.Hello`)
if err != nil {
t.Fatal(err)
}
v := evalCheck(t, i, `foo.Hello`)
f, ok := v.Interface().(func() string)
if !ok {
t.Fatal("conversion failed")
@@ -77,3 +56,52 @@ func TestEval3(t *testing.T) {
t.Fatalf("expected hello, got %v", s)
}
}
func TestEvalNil0(t *testing.T) {
i := interp.New(interp.Opt{})
evalCheck(t, i, `func getNil() error { return nil }`)
v := evalCheck(t, i, `getNil()`)
if !v.IsNil() {
t.Fatalf("expected nil, got %v", v)
}
}
func TestEvalNil1(t *testing.T) {
i := interp.New(interp.Opt{})
evalCheck(t, i, `
package bar
func New() func(string) error {
return func(v string) error {
return nil
}
}
`)
v := evalCheck(t, i, `bar.New()`)
fn, ok := v.Interface().(func(string) error)
if !ok {
t.Fatal("conversion failed")
}
if res := fn("hello"); res != nil {
t.Fatalf("expected nil, got %v", res)
}
}
func TestEvalNil2(t *testing.T) {
i := interp.New(interp.Opt{})
_, err := i.Eval(`a := nil`)
if err.Error() != "1:27: use of untyped nil" {
t.Fatal("should have failed")
}
}
func evalCheck(t *testing.T, i *interp.Interpreter, src string) reflect.Value {
res, err := i.Eval(src)
if err != nil {
t.Fatal(err)
}
return res
}

View File

@@ -1116,8 +1116,8 @@ func _case(n *Node) {
typ := types[0]
n.exec = func(f *Frame) Builtin {
if v := value(f); !v.IsValid() {
// match zero value against interface{}
if typ.cat == UnsetT {
// match zero value against nil
if typ.cat == NilT {
return tnext
}
return fnext

View File

@@ -10,7 +10,7 @@ type Cat uint
// Types for go language
const (
UnsetT Cat = iota
NilT Cat = iota
AliasT
ArrayT
BinT
@@ -48,7 +48,7 @@ const (
)
var cats = [...]string{
UnsetT: "UnsetT",
NilT: "NilT",
AliasT: "AliasT",
ArrayT: "ArrayT",
BinT: "BinT",
@@ -346,7 +346,7 @@ func init() {
zeroValues[ByteT] = reflect.ValueOf(byte(0))
zeroValues[Complex64T] = reflect.ValueOf(complex64(0))
zeroValues[Complex128T] = reflect.ValueOf(complex128(0))
zeroValues[ErrorT] = reflect.ValueOf(error(nil))
zeroValues[ErrorT] = reflect.ValueOf(new(error)).Elem()
zeroValues[Float32T] = reflect.ValueOf(float32(0))
zeroValues[Float64T] = reflect.ValueOf(float64(0))
zeroValues[IntT] = reflect.ValueOf(int(0))
@@ -509,8 +509,7 @@ func (t *Type) TypeOf() reflect.Type {
return reflect.ChanOf(reflect.BothDir, t.val.TypeOf())
case ErrorT:
var e = new(error)
return reflect.TypeOf(e).Elem()
return reflect.TypeOf(new(error)).Elem()
case FuncT:
in := make([]reflect.Type, len(t.arg))