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

View File

@@ -252,6 +252,24 @@ func New(options Options) *Interpreter {
return &i 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 { func initUniverse() *scope {
sc := &scope{global: true, sym: map[string]*symbol{ sc := &scope{global: true, sym: map[string]*symbol{
// predefined Go types // predefined Go types
@@ -286,21 +304,21 @@ func initUniverse() *scope {
"nil": {typ: &itype{cat: nilT, untyped: true}}, "nil": {typ: &itype{cat: nilT, untyped: true}},
// predefined Go builtins // predefined Go builtins
"append": {kind: bltnSym, builtin: _append}, bltnAppend: {kind: bltnSym, builtin: _append},
"cap": {kind: bltnSym, builtin: _cap}, bltnCap: {kind: bltnSym, builtin: _cap},
"close": {kind: bltnSym, builtin: _close}, bltnClose: {kind: bltnSym, builtin: _close},
"complex": {kind: bltnSym, builtin: _complex}, bltnComplex: {kind: bltnSym, builtin: _complex},
"imag": {kind: bltnSym, builtin: _imag}, bltnImag: {kind: bltnSym, builtin: _imag},
"copy": {kind: bltnSym, builtin: _copy}, bltnCopy: {kind: bltnSym, builtin: _copy},
"delete": {kind: bltnSym, builtin: _delete}, bltnDelete: {kind: bltnSym, builtin: _delete},
"len": {kind: bltnSym, builtin: _len}, bltnLen: {kind: bltnSym, builtin: _len},
"make": {kind: bltnSym, builtin: _make}, bltnMake: {kind: bltnSym, builtin: _make},
"new": {kind: bltnSym, builtin: _new}, bltnNew: {kind: bltnSym, builtin: _new},
"panic": {kind: bltnSym, builtin: _panic}, bltnPanic: {kind: bltnSym, builtin: _panic},
"print": {kind: bltnSym, builtin: _print}, bltnPrint: {kind: bltnSym, builtin: _print},
"println": {kind: bltnSym, builtin: _println}, bltnPrintln: {kind: bltnSym, builtin: _println},
"real": {kind: bltnSym, builtin: _real}, bltnReal: {kind: bltnSym, builtin: _real},
"recover": {kind: bltnSym, builtin: _recover}, bltnRecover: {kind: bltnSym, builtin: _recover},
}} }}
return sc 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: `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: `string(append([]byte("hello "), "world"...))`, res: "hello world"},
{src: `e := "world"; string(append([]byte("hello "), e...))`, 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: `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. // Builtin types are special and may depend from their input arguments.
t.cat = builtinT t.cat = builtinT
switch n.child[0].ident { switch n.child[0].ident {
case "complex": case bltnComplex:
var nt0, nt1 *itype var nt0, nt1 *itype
if nt0, err = nodeType(interp, sc, n.child[1]); err != nil { if nt0, err = nodeType(interp, sc, n.child[1]); err != nil {
return nil, err return nil, err
@@ -299,7 +299,7 @@ func nodeType(interp *Interpreter, sc *scope, n *node) (*itype, error) {
case isFloat64(t0) && isFloat64(t1): case isFloat64(t0) && isFloat64(t1):
t = sc.getType("complex128") t = sc.getType("complex128")
case nt0.untyped && isNumber(t0) && nt1.untyped && isNumber(t1): 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): case nt0.untyped && isFloat32(t1) || nt1.untyped && isFloat32(t0):
t = sc.getType("complex64") t = sc.getType("complex64")
case nt0.untyped && isFloat64(t1) || nt1.untyped && isFloat64(t0): 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 t.untyped = true
} }
} }
case "real", "imag": case bltnReal, bltnImag:
if t, err = nodeType(interp, sc, n.child[1]); err != nil { if t, err = nodeType(interp, sc, n.child[1]); err != nil {
return nil, err 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) err = n.cfgErrorf("invalid complex type %s", k)
} }
} }
case "cap", "copy", "len": case bltnCap, bltnCopy, bltnLen:
t = sc.getType("int") t = sc.getType("int")
case "append", "make": case bltnAppend, bltnMake:
t, err = nodeType(interp, sc, n.child[1]) t, err = nodeType(interp, sc, n.child[1])
case "new": case bltnNew:
t, err = nodeType(interp, sc, n.child[1]) t, err = nodeType(interp, sc, n.child[1])
t = &itype{cat: ptrT, val: t, incomplete: t.incomplete, scope: sc} t = &itype{cat: ptrT, val: t, incomplete: t.incomplete, scope: sc}
case "recover": case bltnRecover:
t = sc.getType("interface{}") t = sc.getType("interface{}")
} }
if err != nil { if err != nil {

View File

@@ -630,41 +630,288 @@ func (check typecheck) conversion(n *node, typ *itype) error {
return nil 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. // arguments type checks the call expression arguments.
func (check typecheck) arguments(n *node, child []*node, fun *node, ellipsis bool) error { func (check typecheck) arguments(n *node, child []*node, fun *node, ellipsis bool) error {
params := check.unpackParams(child)
l := len(child) l := len(child)
if ellipsis { if ellipsis {
if !fun.typ.isVariadic() { if !fun.typ.isVariadic() {
return n.cfgErrorf("invalid use of ..., corresponding parameter is non-variadic") 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()) 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 var cnt int
for i, arg := range child { for i, param := range params {
ellip := i == l-1 && ellipsis 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 return err
} }
cnt++ cnt++
@@ -679,29 +926,37 @@ func (check typecheck) arguments(n *node, child []*node, fun *node, ellipsis boo
return nil return nil
} }
func (check typecheck) argument(n *node, ftyp *itype, i int, ellipsis bool) error { func (check typecheck) argument(p param, ftyp *itype, i, l int, ellipsis bool) error {
typ := getArg(ftyp, i) atyp := getArg(ftyp, i)
if typ == nil { if atyp == nil {
return n.cfgErrorf("too many arguments") return p.nod.cfgErrorf("too many arguments")
} }
if isCall(n) && n.child[0].typ.numOut() != 1 { if p.typ == nil && isCall(p.nod) && p.nod.child[0].typ.numOut() != 1 {
return n.cfgErrorf("cannot use %s as type %s", n.child[0].typ.id(), typ.id()) 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 ellipsis {
if i != ftyp.numIn()-1 { 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() t := p.Type().TypeOf()
if t.Kind() != reflect.Slice || !(&itype{cat: valueT, rtype: t.Elem()}).assignableTo(typ) { if t.Kind() != reflect.Slice || !(&itype{cat: valueT, rtype: t.Elem()}).assignableTo(atyp) {
return n.cfgErrorf("cannot use %s as type %s", n.typ.id(), (&itype{cat: arrayT, val: typ}).id()) return p.nod.cfgErrorf("cannot use %s as type %s", p.nod.typ.id(), (&itype{cat: arrayT, val: atyp}).id())
} }
return nil return nil
} }
err := check.assignment(n, typ, "") if p.typ != nil {
return err 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 { func getArg(ftyp *itype, i int) *itype {