Skip to content

Commit

Permalink
feat: add call expression (not builtin) type checking
Browse files Browse the repository at this point in the history
This adds type checking to CallExpr (excluding builtin type checking, as that is a PR in its own right) as well as handling any required constant type conversion.

This also changes constant strings and runes to be represented as `constant.Value`. Runes change `rval` type at CFG typing time to avoid having to type at AST time. There are also changes to importSpecs and `stdlib` to account for the string change. With this all `untyped` types should now be `constant.Value`s, although errors are still not returned if this is not the case to be sure we do not break things.

This also fixed a bug in `itype.methods` that would panic if the type was recursive.
  • Loading branch information
nrwiersma committed Aug 14, 2020
1 parent a004809 commit 913680d
Show file tree
Hide file tree
Showing 31 changed files with 557 additions and 177 deletions.
16 changes: 16 additions & 0 deletions _test/a42.go
@@ -0,0 +1,16 @@
package main

import (
"encoding/binary"
"fmt"
)

func main() {
var b [8]byte
binary.LittleEndian.PutUint64(b[:], uint64(1))

fmt.Println(b)
}

// Output:
// [1 0 0 0 0 0 0 0]
5 changes: 4 additions & 1 deletion extract/extract.go
Expand Up @@ -247,6 +247,9 @@ func fixConst(name string, val constant.Value, imports map[string]bool) string {
str string
)
switch val.Kind() {
case constant.String:
tok = "STRING"
str = val.ExactString()
case constant.Int:
tok = "INT"
str = val.ExactString()
Expand All @@ -269,7 +272,7 @@ func fixConst(name string, val constant.Value, imports map[string]bool) string {
imports["go/constant"] = true
imports["go/token"] = true

return fmt.Sprintf("constant.MakeFromLiteral(\"%s\", token.%s, 0)", str, tok)
return fmt.Sprintf("constant.MakeFromLiteral(%q, token.%s, 0)", str, tok)
}

// importPath checks whether pkgIdent is an existing directory relative to
Expand Down
12 changes: 6 additions & 6 deletions internal/genop/genop.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 2 additions & 10 deletions interp/ast.go
Expand Up @@ -487,20 +487,12 @@ func (interp *Interpreter) ast(src, name string) (string, *node, error) {
n.ident = a.Value
switch a.Kind {
case token.CHAR:
// Char cannot be converted to a const here as we cannot tell the type.
v, _, _, _ := strconv.UnquoteChar(a.Value[1:len(a.Value)-1], '\'')
n.rval = reflect.ValueOf(v)
case token.FLOAT:
case token.FLOAT, token.IMAG, token.INT, token.STRING:
v := constant.MakeFromLiteral(a.Value, a.Kind, 0)
n.rval = reflect.ValueOf(v)
case token.IMAG:
v := constant.MakeFromLiteral(a.Value, a.Kind, 0)
n.rval = reflect.ValueOf(v)
case token.INT:
v := constant.MakeFromLiteral(a.Value, a.Kind, 0)
n.rval = reflect.ValueOf(v)
case token.STRING:
v, _ := strconv.Unquote(a.Value)
n.rval = reflect.ValueOf(v)
}
st.push(n, nod)

Expand Down
72 changes: 39 additions & 33 deletions interp/cfg.go
Expand Up @@ -367,10 +367,10 @@ func (interp *Interpreter) cfg(root *node, pkgID string) ([]*node, error) {
case importSpec:
var name, ipath string
if len(n.child) == 2 {
ipath = n.child[1].rval.String()
ipath = constToString(n.child[1].rval)
name = n.child[0].ident
} else {
ipath = n.child[0].rval.String()
ipath = constToString(n.child[0].rval)
name = identifier.FindString(ipath)
}
if interp.binPkg[ipath] != nil && name != "." {
Expand Down Expand Up @@ -477,6 +477,7 @@ func (interp *Interpreter) cfg(root *node, pkgID string) ([]*node, error) {
wireChild(n)
for i := 0; i < n.nleft; i++ {
dest, src := n.child[i], n.child[sbase+i]
updateSym := false
var sym *symbol
var level int
if n.kind == defineStmt || (n.kind == assignStmt && dest.ident == "_") {
Expand Down Expand Up @@ -513,7 +514,7 @@ func (interp *Interpreter) cfg(root *node, pkgID string) ([]*node, error) {
dest.val = src.val
dest.recv = src.recv
dest.findex = sym.index
sym.rval = src.rval
updateSym = true
} else {
sym, level, _ = sc.lookup(dest.ident)
}
Expand All @@ -523,6 +524,10 @@ func (interp *Interpreter) cfg(root *node, pkgID string) ([]*node, error) {
break
}

if updateSym {
sym.typ = dest.typ
sym.rval = src.rval
}
n.findex = dest.findex
n.level = dest.level

Expand Down Expand Up @@ -816,57 +821,53 @@ func (interp *Interpreter) cfg(root *node, pkgID string) ([]*node, error) {
if op, ok := constBltn[n.child[0].ident]; ok && n.anc.action != aAssign {
op(n) // pre-compute non-assigned constant :
}

case n.child[0].isType(sc):
// Type conversion expression
if isInt(n.child[0].typ.TypeOf()) && n.child[1].kind == basicLit && isFloat(n.child[1].typ.TypeOf()) {
err = n.cfgErrorf("truncated to integer")
c0, c1 := n.child[0], n.child[1]
switch len(n.child) {
case 1:
err = n.cfgErrorf("missing argument in conversion to %s", c0.typ.id())
case 2:
err = check.conversion(c1, c0.typ)
default:
err = n.cfgErrorf("too many arguments in conversion to %s", c0.typ.id())
}
if err != nil {
break
}

n.action = aConvert
switch {
case isInterface(n.child[0].typ) && !n.child[1].isNil():
case isInterface(c0.typ) && !c1.isNil():
// Convert to interface: just check that all required methods are defined by concrete type.
c0, c1 := n.child[0], n.child[1]
if !c1.typ.implements(c0.typ) {
err = n.cfgErrorf("type %v does not implement interface %v", c1.typ.id(), c0.typ.id())
}
// Pass value as is
n.gen = nop
n.typ = n.child[1].typ
n.findex = n.child[1].findex
n.level = n.child[1].level
n.val = n.child[1].val
n.rval = n.child[1].rval
case n.child[1].rval.IsValid() && isConstType(n.child[0].typ):
n.typ = c1.typ
n.findex = c1.findex
n.level = c1.level
n.val = c1.val
n.rval = c1.rval
case c1.rval.IsValid() && isConstType(c0.typ):
n.gen = nop
n.findex = -1
n.typ = n.child[0].typ
n.rval = n.child[1].rval
convertConstantValue(n)
n.typ = c0.typ
n.rval = c1.rval
default:
n.gen = convert
n.typ = n.child[0].typ
n.typ = c0.typ
n.findex = sc.add(n.typ)
}
case isBinCall(n):
n.gen = callBin
typ := n.child[0].typ.rtype
numIn := len(n.child) - 1
tni := typ.NumIn()
if numIn == 1 && isCall(n.child[1]) {
numIn = n.child[1].typ.numOut()
}
if n.child[0].action == aGetMethod {
tni-- // The first argument is the method receiver.
}
if typ.IsVariadic() {
tni-- // The last argument could be empty.
}
if numIn < tni {
err = n.cfgErrorf("not enough arguments in call to %v", n.child[0].name())
err = check.arguments(n, n.child[1:], n.child[0], n.action == aCallSlice)
if err != nil {
break
}

n.gen = callBin
typ := n.child[0].typ.rtype
if typ.NumOut() > 0 {
if funcType := n.child[0].typ.val; funcType != nil {
// Use the original unwrapped function type, to allow future field and
Expand All @@ -889,6 +890,11 @@ func (interp *Interpreter) cfg(root *node, pkgID string) ([]*node, error) {
}
}
default:
err = check.arguments(n, n.child[1:], n.child[0], n.action == aCallSlice)
if err != nil {
break
}

if n.child[0].action == aGetFunc {
// Allocate a frame entry to store the anonymous function definition.
sc.add(n.child[0].typ)
Expand Down
4 changes: 2 additions & 2 deletions interp/gta.go
Expand Up @@ -205,10 +205,10 @@ func (interp *Interpreter) gta(root *node, rpath, pkgID string) ([]*node, error)
case importSpec:
var name, ipath string
if len(n.child) == 2 {
ipath = n.child[1].rval.String()
ipath = constToString(n.child[1].rval)
name = n.child[0].ident
} else {
ipath = n.child[0].rval.String()
ipath = constToString(n.child[0].rval)
}
// Try to import a binary package first, or a source package
var pkgName string
Expand Down
71 changes: 70 additions & 1 deletion interp/interp_eval_test.go
Expand Up @@ -75,7 +75,7 @@ func TestEvalAssign(t *testing.T) {
{src: `b := "Hello"; b += 1`, err: "1:42: invalid operation: mismatched types string and int"},
{src: `c := "Hello"; c -= " world"`, err: "1:42: invalid operation: operator -= not defined on string"},
{src: "e := 64.4; e %= 64", err: "1:39: invalid operation: operator %= not defined on float64"},
{src: "f := int64(3.2)", err: "1:33: truncated to integer"},
{src: "f := int64(3.2)", err: "1:39: cannot convert expression of type float64 to type int64"},
{src: "g := 1; g <<= 8", res: "256"},
{src: "h := 1; h >>= 8", res: "0"},
})
Expand Down Expand Up @@ -296,6 +296,8 @@ func Foo() {
func TestEvalComparison(t *testing.T) {
i := interp.New(interp.Options{})
runTests(t, i, []testCase{
{src: `2 > 1`, res: "true"},
{src: `1.2 > 1.1`, res: "true"},
{src: `"hhh" > "ggg"`, res: "true"},
{
desc: "mismatched types",
Expand Down Expand Up @@ -353,6 +355,16 @@ func TestEvalCompositeStruct(t *testing.T) {
})
}

func TestEvalConversion(t *testing.T) {
i := interp.New(interp.Options{})
runTests(t, i, []testCase{
{src: `a := uint64(1)`, res: "1"},
{src: `i := 1.1; a := uint64(i)`, res: "1"},
{src: `b := string(49)`, res: "1"},
{src: `c := uint64(1.1)`, err: "1:40: cannot convert expression of type float64 to type uint64"},
})
}

func TestEvalUnary(t *testing.T) {
i := interp.New(interp.Options{})
runTests(t, i, []testCase{
Expand Down Expand Up @@ -445,6 +457,63 @@ func TestEvalFunctionCallWithFunctionParam(t *testing.T) {
}
}

func TestEvalCall(t *testing.T) {
i := interp.New(interp.Options{})
runTests(t, i, []testCase{
{src: ` test := func(a int, b float64) int { return a }
a := test(1, 2.3)`, res: "1"},
{src: ` test := func(a int, b float64) int { return a }
a := test(1)`, err: "2:10: not enough arguments in call to test"},
{src: ` test := func(a int, b float64) int { return a }
s := "test"
a := test(1, s)`, err: "3:18: cannot use type string as type float64"},
{src: ` test := func(a ...int) int { return 1 }
a := test([]int{1}...)`, res: "1"},
{src: ` test := func(a ...int) int { return 1 }
a := test()`, res: "1"},
{src: ` test := func(a ...int) int { return 1 }
blah := func() []int { return []int{1,1} }
a := test(blah()...)`, res: "1"},
{src: ` test := func(a ...int) int { return 1 }
a := test([]string{"1"}...)`, err: "2:15: cannot use []string as type []int"},
{src: ` test := func(a ...int) int { return 1 }
i := 1
a := test(i...)`, err: "3:15: cannot use int as type []int"},
{src: ` test := func(a int) int { return a }
a := test([]int{1}...)`, err: "2:10: invalid use of ..., corresponding parameter is non-variadic"},
{src: ` test := func(a ...int) int { return 1 }
blah := func() (int, int) { return 1, 1 }
a := test(blah()...)`, err: "3:15: cannot use ... with 2-valued func()(int,int)"},
{src: ` test := func(a, b int) int { return a }
blah := func() (int, int) { return 1, 1 }
a := test(blah())`, res: "1"},
{src: ` test := func(a, b int) int { return a }
blah := func() int { return 1 }
a := test(blah(), blah())`, res: "1"},
{src: ` test := func(a, b, c, d int) int { return a }
blah := func() (int, int) { return 1, 1 }
a := test(blah(), blah())`, err: "3:15: cannot use func()(int,int) as type int"},
{src: ` test := func(a, b int) int { return a }
blah := func() (int, float64) { return 1, 1.1 }
a := test(blah())`, err: "3:15: cannot use func()(int,float64) as type (int,int)"},
})
}

func TestEvalBinCall(t *testing.T) {
i := interp.New(interp.Options{})
i.Use(stdlib.Symbols)
if _, err := i.Eval(`import "fmt"`); err != nil {
t.Fatal(err)
}
runTests(t, i, []testCase{
{src: `a := fmt.Sprint(1, 2.3)`, res: "1 2.3"},
{src: `a := fmt.Sprintf()`, err: "1:33: not enough arguments in call to fmt.Sprintf"},
{src: `i := 1
a := fmt.Sprintf(i)`, err: "2:24: cannot use type int as type string"},
{src: `a := fmt.Sprint()`, res: ""},
})
}

func TestEvalMissingSymbol(t *testing.T) {
defer func() {
r := recover()
Expand Down

0 comments on commit 913680d

Please sign in to comment.