From f3f9ffaea7c191f124def4c4662e6730654127ad Mon Sep 17 00:00:00 2001 From: Nicholas Wiersma Date: Thu, 27 Aug 2020 14:02:04 +0200 Subject: [PATCH] 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. --- interp/cfg.go | 11 +- interp/interp.go | 48 ++++-- interp/interp_eval_test.go | 32 ++++ interp/type.go | 14 +- interp/typecheck.go | 323 +++++++++++++++++++++++++++++++++---- 5 files changed, 369 insertions(+), 59 deletions(-) diff --git a/interp/cfg.go b/interp/cfg.go index 7403adb6..5adaf1dd 100644 --- a/interp/cfg.go +++ b/interp/cfg.go @@ -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 { diff --git a/interp/interp.go b/interp/interp.go index 497b7aec..0b0d6abd 100644 --- a/interp/interp.go +++ b/interp/interp.go @@ -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 } diff --git a/interp/interp_eval_test.go b/interp/interp_eval_test.go index beb75076..3c2dbf23 100644 --- a/interp/interp_eval_test.go +++ b/interp/interp_eval_test.go @@ -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"}, }) } diff --git a/interp/type.go b/interp/type.go index 5a2b4a98..dff5636d 100644 --- a/interp/type.go +++ b/interp/type.go @@ -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 { diff --git a/interp/typecheck.go b/interp/typecheck.go index 00837739..055193b7 100644 --- a/interp/typecheck.go +++ b/interp/typecheck.go @@ -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 {