From 36b6601baf63e946aaa2df72dd782563d43f3edd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 14 Jan 2015 11:47:20 -0800 Subject: [PATCH] config/lang: implicit type conversions --- config/lang/check_types.go | 59 +++++++++++++++++++++- config/lang/check_types_test.go | 89 +++++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+), 2 deletions(-) diff --git a/config/lang/check_types.go b/config/lang/check_types.go index 5015781e8..1a0fb1b8d 100644 --- a/config/lang/check_types.go +++ b/config/lang/check_types.go @@ -9,9 +9,21 @@ import ( // TypeCheck implements ast.Visitor for type checking an AST tree. // It requires some configuration to look up the type of nodes. +// +// It also optionally will not type error and will insert an implicit +// type conversions for specific types if specified by the Implicit +// field. Note that this is kind of organizationally weird to put into +// this structure but we'd rather do that than duplicate the type checking +// logic multiple times. type TypeCheck struct { Scope *Scope + // Implicit is a map of implicit type conversions that we can do, + // and that shouldn't error. The key of the first map is the from type, + // the key of the second map is the to type, and the final string + // value is the function to call (which must be registered in the Scope). + Implicit map[ast.Type]map[ast.Type]string + stack []ast.Type err error lock sync.Mutex @@ -61,6 +73,12 @@ func (v *TypeCheck) visitCall(n *ast.Call) { // Verify the args for i, expected := range function.ArgTypes { if args[i] != expected { + cn := v.implicitConversion(args[i], expected, n.Args[i]) + if cn != nil { + n.Args[i] = cn + continue + } + v.createErr(n, fmt.Sprintf( "%s: argument %d should be %s, got %s", n.Func, i+1, expected, args[i])) @@ -73,9 +91,17 @@ func (v *TypeCheck) visitCall(n *ast.Call) { args = args[len(function.ArgTypes):] for i, t := range args { if t != function.VariadicType { + realI := i + len(function.ArgTypes) + cn := v.implicitConversion( + t, function.VariadicType, n.Args[realI]) + if cn != nil { + n.Args[realI] = cn + continue + } + v.createErr(n, fmt.Sprintf( "%s: argument %d should be %s, got %s", - n.Func, i+len(function.ArgTypes), + n.Func, realI, function.VariadicType, t)) return } @@ -95,8 +121,14 @@ func (v *TypeCheck) visitConcat(n *ast.Concat) { // All concat args must be strings, so validate that for i, t := range types { if t != ast.TypeString { + cn := v.implicitConversion(t, ast.TypeString, n.Exprs[i]) + if cn != nil { + n.Exprs[i] = cn + continue + } + v.createErr(n, fmt.Sprintf( - "argument %d must be a sting", n, i+1)) + "argument %d must be a string", i+1)) return } } @@ -126,6 +158,29 @@ func (v *TypeCheck) createErr(n ast.Node, str string) { v.err = fmt.Errorf("%s: %s", n.Pos(), str) } +func (v *TypeCheck) implicitConversion( + actual ast.Type, expected ast.Type, n ast.Node) ast.Node { + if v.Implicit == nil { + return nil + } + + fromMap, ok := v.Implicit[actual] + if !ok { + return nil + } + + toFunc, ok := fromMap[expected] + if !ok { + return nil + } + + return &ast.Call{ + Func: toFunc, + Args: []ast.Node{n}, + Posx: n.Pos(), + } +} + func (v *TypeCheck) reset() { v.stack = nil v.err = nil diff --git a/config/lang/check_types_test.go b/config/lang/check_types_test.go index f39ff8743..292d9255d 100644 --- a/config/lang/check_types_test.go +++ b/config/lang/check_types_test.go @@ -174,3 +174,92 @@ func TestTypeCheck(t *testing.T) { } } } + +func TestTypeCheck_implicit(t *testing.T) { + implicitMap := map[ast.Type]map[ast.Type]string{ + ast.TypeInt: { + ast.TypeString: "intToString", + }, + } + + cases := []struct { + Input string + Scope *Scope + Error bool + }{ + { + "foo ${bar}", + &Scope{ + VarMap: map[string]Variable{ + "bar": Variable{ + Value: 42, + Type: ast.TypeInt, + }, + }, + }, + false, + }, + + { + "foo ${foo(42)}", + &Scope{ + FuncMap: map[string]Function{ + "foo": Function{ + ArgTypes: []ast.Type{ast.TypeString}, + ReturnType: ast.TypeString, + }, + }, + }, + false, + }, + + { + `foo ${foo("42", 42)}`, + &Scope{ + FuncMap: map[string]Function{ + "foo": Function{ + ArgTypes: []ast.Type{ast.TypeString}, + Variadic: true, + VariadicType: ast.TypeString, + ReturnType: ast.TypeString, + }, + }, + }, + false, + }, + } + + for _, tc := range cases { + node, err := Parse(tc.Input) + if err != nil { + t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input) + } + + // Modify the scope to add our conversion functions. + if tc.Scope.FuncMap == nil { + tc.Scope.FuncMap = make(map[string]Function) + } + tc.Scope.FuncMap["intToString"] = Function{ + ArgTypes: []ast.Type{ast.TypeInt}, + ReturnType: ast.TypeString, + } + + // Do the first pass... + visitor := &TypeCheck{Scope: tc.Scope, Implicit: implicitMap} + err = visitor.Visit(node) + if (err != nil) != tc.Error { + t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input) + } + if err != nil { + continue + } + + // If we didn't error, then the next type check should not fail + // WITHOUT implicits. + visitor = &TypeCheck{Scope: tc.Scope} + err = visitor.Visit(node) + if err != nil { + t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input) + } + } +}