feat: add builtin type checking

This adds type checking for builtin functions. It also refactors builtin names into constants due to the number of times they are used.
This commit is contained in:
Nicholas Wiersma
2020-08-27 14:02:04 +02:00
committed by GitHub
parent e332a6b3be
commit f3f9ffaea7
5 changed files with 369 additions and 59 deletions

View File

@@ -39,9 +39,9 @@ var constOp = map[action]func(*node){
}
var constBltn = map[string]func(*node){
"complex": complexConst,
"imag": imagConst,
"real": realConst,
bltnComplex: complexConst,
bltnImag: imagConst,
bltnReal: realConst,
}
var identifier = regexp.MustCompile(`([\pL_][\pL_\d]*)$`)
@@ -799,6 +799,11 @@ func (interp *Interpreter) cfg(root *node, importPath string) ([]*node, error) {
wireChild(n)
switch {
case interp.isBuiltinCall(n):
err = check.builtin(n.child[0].ident, n, n.child[1:], n.action == aCallSlice)
if err != nil {
break
}
n.gen = n.child[0].sym.builtin
n.child[0].typ = &itype{cat: builtinT}
if n.typ, err = nodeType(interp, sc, n); err != nil {

View File

@@ -252,6 +252,24 @@ func New(options Options) *Interpreter {
return &i
}
const (
bltnAppend = "append"
bltnCap = "cap"
bltnClose = "close"
bltnComplex = "complex"
bltnImag = "imag"
bltnCopy = "copy"
bltnDelete = "delete"
bltnLen = "len"
bltnMake = "make"
bltnNew = "new"
bltnPanic = "panic"
bltnPrint = "print"
bltnPrintln = "println"
bltnReal = "real"
bltnRecover = "recover"
)
func initUniverse() *scope {
sc := &scope{global: true, sym: map[string]*symbol{
// predefined Go types
@@ -286,21 +304,21 @@ func initUniverse() *scope {
"nil": {typ: &itype{cat: nilT, untyped: true}},
// predefined Go builtins
"append": {kind: bltnSym, builtin: _append},
"cap": {kind: bltnSym, builtin: _cap},
"close": {kind: bltnSym, builtin: _close},
"complex": {kind: bltnSym, builtin: _complex},
"imag": {kind: bltnSym, builtin: _imag},
"copy": {kind: bltnSym, builtin: _copy},
"delete": {kind: bltnSym, builtin: _delete},
"len": {kind: bltnSym, builtin: _len},
"make": {kind: bltnSym, builtin: _make},
"new": {kind: bltnSym, builtin: _new},
"panic": {kind: bltnSym, builtin: _panic},
"print": {kind: bltnSym, builtin: _print},
"println": {kind: bltnSym, builtin: _println},
"real": {kind: bltnSym, builtin: _real},
"recover": {kind: bltnSym, builtin: _recover},
bltnAppend: {kind: bltnSym, builtin: _append},
bltnCap: {kind: bltnSym, builtin: _cap},
bltnClose: {kind: bltnSym, builtin: _close},
bltnComplex: {kind: bltnSym, builtin: _complex},
bltnImag: {kind: bltnSym, builtin: _imag},
bltnCopy: {kind: bltnSym, builtin: _copy},
bltnDelete: {kind: bltnSym, builtin: _delete},
bltnLen: {kind: bltnSym, builtin: _len},
bltnMake: {kind: bltnSym, builtin: _make},
bltnNew: {kind: bltnSym, builtin: _new},
bltnPanic: {kind: bltnSym, builtin: _panic},
bltnPrint: {kind: bltnSym, builtin: _print},
bltnPrintln: {kind: bltnSym, builtin: _println},
bltnReal: {kind: bltnSym, builtin: _real},
bltnRecover: {kind: bltnSym, builtin: _recover},
}}
return sc
}

View File

@@ -99,7 +99,39 @@ func TestEvalBuiltin(t *testing.T) {
{src: `c := []int{1}; d := []int{2, 3}; c = append(c, d...); c`, res: "[1 2 3]"},
{src: `string(append([]byte("hello "), "world"...))`, res: "hello world"},
{src: `e := "world"; string(append([]byte("hello "), e...))`, res: "hello world"},
{src: `b := []int{1}; b = append(1, 2, 3); b`, err: "1:54: first argument to append must be slice; have int"},
{src: `g := len(a)`, res: "1"},
{src: `g := cap(a)`, res: "1"},
{src: `g := len("test")`, res: "4"},
{src: `g := len(map[string]string{"a": "b"})`, res: "1"},
{src: `a := len()`, err: "not enough arguments in call to len"},
{src: `a := len([]int, 0)`, err: "too many arguments for len"},
{src: `g := cap("test")`, err: "1:37: invalid argument for cap"},
{src: `g := cap(map[string]string{"a": "b"})`, err: "1:37: invalid argument for cap"},
{src: `h := make(chan int, 1); close(h); len(h)`, res: "0"},
{src: `close(a)`, err: "1:34: invalid operation: non-chan type []int"},
{src: `h := make(chan int, 1); var i <-chan int = h; close(i)`, err: "1:80: invalid operation: cannot close receive-only channel"},
{src: `j := make([]int, 2)`, res: "[0 0]"},
{src: `j := make([]int, 2, 3)`, res: "[0 0]"},
{src: `j := make(int)`, err: "1:38: cannot make int; type must be slice, map, or channel"},
{src: `j := make([]int)`, err: "1:33: not enough arguments in call to make"},
{src: `j := make([]int, 0, 1, 2)`, err: "1:33: too many arguments for make"},
{src: `j := make([]int, 2, 1)`, err: "1:33: len larger than cap in make"},
{src: `j := make([]int, "test")`, err: "1:45: cannot convert \"test\" to int"},
{src: `k := []int{3, 4}; copy(k, []int{1,2}); k`, res: "[1 2]"},
{src: `f := []byte("Hello"); copy(f, "world"); string(f)`, res: "world"},
{src: `copy(g, g)`, err: "1:28: copy expects slice arguments"},
{src: `copy(a, "world")`, err: "1:28: arguments to copy have different element types []int and string"},
{src: `l := map[string]int{"a": 1, "b": 2}; delete(l, "a"); l`, res: "map[b:2]"},
{src: `delete(a, 1)`, err: "1:35: first argument to delete must be map; have []int"},
{src: `l := map[string]int{"a": 1, "b": 2}; delete(l, 1)`, err: "1:75: cannot use int as type string in delete"},
{src: `a := []int{1,2}; println(a...)`, err: "invalid use of ... with builtin println"},
{src: `m := complex(3, 2); real(m)`, res: "3"},
{src: `m := complex(3, 2); imag(m)`, res: "2"},
{src: `m := complex("test", 2)`, err: "1:33: invalid types string and int"},
{src: `imag("test")`, err: "1:33: cannot convert \"test\" to complex128"},
{src: `imag(a)`, err: "1:33: invalid argument type []int for imag"},
{src: `real(a)`, err: "1:33: invalid argument type []int for real"},
})
}

View File

@@ -282,7 +282,7 @@ func nodeType(interp *Interpreter, sc *scope, n *node) (*itype, error) {
// Builtin types are special and may depend from their input arguments.
t.cat = builtinT
switch n.child[0].ident {
case "complex":
case bltnComplex:
var nt0, nt1 *itype
if nt0, err = nodeType(interp, sc, n.child[1]); err != nil {
return nil, err
@@ -299,7 +299,7 @@ func nodeType(interp *Interpreter, sc *scope, n *node) (*itype, error) {
case isFloat64(t0) && isFloat64(t1):
t = sc.getType("complex128")
case nt0.untyped && isNumber(t0) && nt1.untyped && isNumber(t1):
t = &itype{cat: valueT, rtype: complexType, scope: sc}
t = untypedComplex
case nt0.untyped && isFloat32(t1) || nt1.untyped && isFloat32(t0):
t = sc.getType("complex64")
case nt0.untyped && isFloat64(t1) || nt1.untyped && isFloat64(t0):
@@ -311,7 +311,7 @@ func nodeType(interp *Interpreter, sc *scope, n *node) (*itype, error) {
t.untyped = true
}
}
case "real", "imag":
case bltnReal, bltnImag:
if t, err = nodeType(interp, sc, n.child[1]); err != nil {
return nil, err
}
@@ -327,14 +327,14 @@ func nodeType(interp *Interpreter, sc *scope, n *node) (*itype, error) {
err = n.cfgErrorf("invalid complex type %s", k)
}
}
case "cap", "copy", "len":
case bltnCap, bltnCopy, bltnLen:
t = sc.getType("int")
case "append", "make":
case bltnAppend, bltnMake:
t, err = nodeType(interp, sc, n.child[1])
case "new":
case bltnNew:
t, err = nodeType(interp, sc, n.child[1])
t = &itype{cat: ptrT, val: t, incomplete: t.incomplete, scope: sc}
case "recover":
case bltnRecover:
t = sc.getType("interface{}")
}
if err != nil {

View File

@@ -630,41 +630,288 @@ func (check typecheck) conversion(n *node, typ *itype) error {
return nil
}
type param struct {
nod *node
typ *itype
}
func (p param) Type() *itype {
if p.typ != nil {
return p.typ
}
return p.nod.typ
}
// unpackParams unpacks child parameters into a slice of param.
// If there is only 1 child and it is a callExpr with an n-value return,
// the return types are returned, otherwise the original child nodes are
// returned with nil typ.
func (check typecheck) unpackParams(child []*node) (params []param) {
if len(child) == 1 && isCall(child[0]) && child[0].child[0].typ.numOut() > 1 {
c0 := child[0]
ftyp := child[0].child[0].typ
for i := 0; i < ftyp.numOut(); i++ {
params = append(params, param{nod: c0, typ: ftyp.out(i)})
}
return params
}
for _, c := range child {
params = append(params, param{nod: c})
}
return params
}
var builtinFuncs = map[string]struct {
args int
variadic bool
}{
bltnAppend: {args: 1, variadic: true},
bltnCap: {args: 1, variadic: false},
bltnClose: {args: 1, variadic: false},
bltnComplex: {args: 2, variadic: false},
bltnImag: {args: 1, variadic: false},
bltnCopy: {args: 2, variadic: false},
bltnDelete: {args: 2, variadic: false},
bltnLen: {args: 1, variadic: false},
bltnMake: {args: 1, variadic: true},
bltnNew: {args: 1, variadic: false},
bltnPanic: {args: 1, variadic: false},
bltnPrint: {args: 0, variadic: true},
bltnPrintln: {args: 0, variadic: true},
bltnReal: {args: 1, variadic: false},
bltnRecover: {args: 0, variadic: false},
}
func (check typecheck) builtin(name string, n *node, child []*node, ellipsis bool) error {
fun := builtinFuncs[name]
if ellipsis && name != bltnAppend {
return n.cfgErrorf("invalid use of ... with builtin %s", name)
}
var params []param
nparams := len(child)
switch name {
case bltnMake, bltnNew:
// Special param handling
default:
params = check.unpackParams(child)
nparams = len(params)
}
if nparams < fun.args {
return n.cfgErrorf("not enough arguments in call to %s", name)
} else if !fun.variadic && nparams > fun.args {
return n.cfgErrorf("too many arguments for %s", name)
}
switch name {
case bltnAppend:
typ := params[0].Type()
t := typ.TypeOf()
if t.Kind() != reflect.Slice {
return params[0].nod.cfgErrorf("first argument to append must be slice; have %s", typ.id())
}
// Special case append([]byte, "test"...) is allowed.
t1 := params[1].Type()
if nparams == 2 && ellipsis && t.Elem().Kind() == reflect.Uint8 && t1.TypeOf().Kind() == reflect.String {
if t1.untyped {
return check.convertUntyped(params[1].nod, &itype{cat: stringT, name: "string"})
}
return nil
}
// We cannot check a recursive type.
if isRecursiveType(typ, typ.TypeOf()) {
return nil
}
fun := &node{
typ: &itype{
cat: funcT,
arg: []*itype{
typ,
{cat: variadicT, val: &itype{cat: valueT, rtype: t.Elem()}},
},
ret: []*itype{typ},
},
ident: "append",
}
return check.arguments(n, child, fun, ellipsis)
case bltnCap, bltnLen:
typ := arrayDeref(params[0].Type())
ok := false
switch typ.TypeOf().Kind() {
case reflect.Array, reflect.Slice, reflect.Chan:
ok = true
case reflect.String, reflect.Map:
ok = name == bltnLen
}
if !ok {
return params[0].nod.cfgErrorf("invalid argument for %s", name)
}
case bltnClose:
p := params[0]
typ := p.Type()
t := typ.TypeOf()
if t.Kind() != reflect.Chan {
return p.nod.cfgErrorf("invalid operation: non-chan type %s", p.nod.typ.id())
}
if t.ChanDir() == reflect.RecvDir {
return p.nod.cfgErrorf("invalid operation: cannot close receive-only channel")
}
case bltnComplex:
var err error
p0, p1 := params[0], params[1]
typ0, typ1 := p0.Type(), p1.Type()
switch {
case typ0.untyped && !typ1.untyped:
err = check.convertUntyped(p0.nod, typ1)
case !typ0.untyped && typ1.untyped:
err = check.convertUntyped(p1.nod, typ0)
case typ0.untyped && typ1.untyped:
fltType := &itype{cat: float64T, name: "float64"}
err = check.convertUntyped(p0.nod, fltType)
if err != nil {
break
}
err = check.convertUntyped(p1.nod, fltType)
}
if err != nil {
return err
}
// check we have the correct types after conversion.
typ0, typ1 = p0.Type(), p1.Type()
if !typ0.equals(typ1) {
return n.cfgErrorf("invalid operation: mismatched types %s and %s", typ0.id(), typ1.id())
}
if !isFloat(typ0.TypeOf()) {
return n.cfgErrorf("invalid operation: arguments have type %s, expected floating-point", typ0.id())
}
case bltnImag, bltnReal:
p := params[0]
typ := p.Type()
if typ.untyped {
if err := check.convertUntyped(p.nod, &itype{cat: complex128T, name: "complex128"}); err != nil {
return err
}
}
typ = p.Type()
if !isComplex(typ.TypeOf()) {
return p.nod.cfgErrorf("invalid argument type %s for %s", typ.id(), name)
}
case bltnCopy:
typ0, typ1 := params[0].Type(), params[1].Type()
var t0, t1 reflect.Type
if t := typ0.TypeOf(); t.Kind() == reflect.Slice {
t0 = t.Elem()
}
switch t := typ1.TypeOf(); t.Kind() {
case reflect.String:
t1 = reflect.TypeOf(byte(1))
case reflect.Slice:
t1 = t.Elem()
}
if t0 == nil || t1 == nil {
return n.cfgErrorf("copy expects slice arguments")
}
if !reflect.DeepEqual(t0, t1) {
return n.cfgErrorf("arguments to copy have different element types %s and %s", typ0.id(), typ1.id())
}
case bltnDelete:
typ := params[0].Type()
if typ.TypeOf().Kind() != reflect.Map {
return params[0].nod.cfgErrorf("first argument to delete must be map; have %s", typ.id())
}
ktyp := params[1].Type()
if !ktyp.assignableTo(typ.key) {
return params[1].nod.cfgErrorf("cannot use %s as type %s in delete", ktyp.id(), typ.key.id())
}
case bltnMake:
var min int
switch child[0].typ.TypeOf().Kind() {
case reflect.Slice:
min = 2
case reflect.Map, reflect.Chan:
min = 1
default:
return child[0].cfgErrorf("cannot make %s; type must be slice, map, or channel", child[0].typ.id())
}
if nparams < min {
return n.cfgErrorf("not enough arguments in call to make")
} else if nparams > min+1 {
return n.cfgErrorf("too many arguments for make")
}
var sizes []int
for _, c := range child[1:] {
if err := check.index(c, -1); err != nil {
return err
}
if c.rval.IsValid() {
sizes = append(sizes, int(vInt(c.rval)))
}
}
for len(sizes) == 2 && sizes[0] > sizes[1] {
return n.cfgErrorf("len larger than cap in make")
}
case bltnPanic:
return check.assignment(params[0].nod, &itype{cat: interfaceT}, "argument to panic")
case bltnPrint, bltnPrintln:
for _, param := range params {
if param.typ != nil {
continue
}
if err := check.assignment(param.nod, nil, "argument to "+name); err != nil {
return err
}
}
case bltnRecover, bltnNew:
// Nothing to do.
default:
return n.cfgErrorf("unsupported builtin %s", name)
}
return nil
}
// arrayDeref returns A if typ is *A, otherwise typ.
func arrayDeref(typ *itype) *itype {
if typ.cat == valueT && typ.TypeOf().Kind() == reflect.Ptr {
t := typ.TypeOf()
if t.Elem().Kind() == reflect.Array {
return &itype{cat: valueT, rtype: t.Elem()}
}
return typ
}
if typ.cat == ptrT && typ.val.cat == arrayT && typ.val.sizedef {
return typ.val
}
return typ
}
// arguments type checks the call expression arguments.
func (check typecheck) arguments(n *node, child []*node, fun *node, ellipsis bool) error {
params := check.unpackParams(child)
l := len(child)
if ellipsis {
if !fun.typ.isVariadic() {
return n.cfgErrorf("invalid use of ..., corresponding parameter is non-variadic")
}
if len(child) == 1 && isCall(child[0]) && child[0].child[0].typ.numOut() > 1 {
if len(params) > l {
return child[0].cfgErrorf("cannot use ... with %d-valued %s", child[0].child[0].typ.numOut(), child[0].child[0].typ.id())
}
}
if len(child) == 1 && isCall(child[0]) && child[0].child[0].typ.numOut() > 1 {
// Handle the case of unpacking a n-valued function into the params.
c := child[0].child[0]
l := c.typ.numOut()
if l < fun.typ.numIn() {
return child[0].cfgErrorf("not enough arguments in call to %s", fun.name())
}
for i := 0; i < l; i++ {
arg := getArg(fun.typ, i)
if arg == nil {
return child[0].cfgErrorf("too many arguments")
}
if !c.typ.out(i).assignableTo(arg) {
return child[0].cfgErrorf("cannot use %s as type %s", c.typ.id(), getArgsID(fun.typ))
}
}
return nil
}
var cnt int
for i, arg := range child {
for i, param := range params {
ellip := i == l-1 && ellipsis
if err := check.argument(arg, fun.typ, cnt, ellip); err != nil {
if err := check.argument(param, fun.typ, cnt, l, ellip); err != nil {
return err
}
cnt++
@@ -679,29 +926,37 @@ func (check typecheck) arguments(n *node, child []*node, fun *node, ellipsis boo
return nil
}
func (check typecheck) argument(n *node, ftyp *itype, i int, ellipsis bool) error {
typ := getArg(ftyp, i)
if typ == nil {
return n.cfgErrorf("too many arguments")
func (check typecheck) argument(p param, ftyp *itype, i, l int, ellipsis bool) error {
atyp := getArg(ftyp, i)
if atyp == nil {
return p.nod.cfgErrorf("too many arguments")
}
if isCall(n) && n.child[0].typ.numOut() != 1 {
return n.cfgErrorf("cannot use %s as type %s", n.child[0].typ.id(), typ.id())
if p.typ == nil && isCall(p.nod) && p.nod.child[0].typ.numOut() != 1 {
if l == 1 {
return p.nod.cfgErrorf("cannot use %s as type %s", p.nod.child[0].typ.id(), getArgsID(ftyp))
}
return p.nod.cfgErrorf("cannot use %s as type %s", p.nod.child[0].typ.id(), atyp.id())
}
if ellipsis {
if i != ftyp.numIn()-1 {
return n.cfgErrorf("can only use ... with matching parameter")
return p.nod.cfgErrorf("can only use ... with matching parameter")
}
t := n.typ.TypeOf()
if t.Kind() != reflect.Slice || !(&itype{cat: valueT, rtype: t.Elem()}).assignableTo(typ) {
return n.cfgErrorf("cannot use %s as type %s", n.typ.id(), (&itype{cat: arrayT, val: typ}).id())
t := p.Type().TypeOf()
if t.Kind() != reflect.Slice || !(&itype{cat: valueT, rtype: t.Elem()}).assignableTo(atyp) {
return p.nod.cfgErrorf("cannot use %s as type %s", p.nod.typ.id(), (&itype{cat: arrayT, val: atyp}).id())
}
return nil
}
err := check.assignment(n, typ, "")
return err
if p.typ != nil {
if !p.typ.assignableTo(atyp) {
return p.nod.cfgErrorf("cannot use %s as type %s", p.nod.child[0].typ.id(), getArgsID(ftyp))
}
return nil
}
return check.assignment(p.nod, atyp, "")
}
func getArg(ftyp *itype, i int) *itype {