diff --git a/config/lang/ast/ast.go b/config/lang/ast/ast.go index dfe863f99e..0d2465b909 100644 --- a/config/lang/ast/ast.go +++ b/config/lang/ast/ast.go @@ -12,6 +12,9 @@ type Node interface { // Pos returns the position of this node in some source. Pos() Pos + + // Type returns the type of this node for the given context. + Type(Scope) (Type, error) } // Pos is the starting position of an AST node @@ -23,6 +26,12 @@ func (p Pos) String() string { return fmt.Sprintf("%d:%d", p.Line, p.Column) } +// EvalContext is the context given for evaluation. +type EvalContext struct { + Scope Scope + Stack Stack +} + // Visitors are just implementations of this function. // // The function must return the Node to replace this node with. "nil" is @@ -40,7 +49,7 @@ type Visitor func(Node) Node //go:generate stringer -type=Type -// Type is the type of a literal. +// Type is the type of any value. type Type uint32 const ( diff --git a/config/lang/ast/call.go b/config/lang/ast/call.go index bbb632b7b8..ace1147a66 100644 --- a/config/lang/ast/call.go +++ b/config/lang/ast/call.go @@ -32,3 +32,12 @@ func (n *Call) String() string { return fmt.Sprintf("Call(%s, %s)", n.Func, strings.Join(args, ", ")) } + +func (n *Call) Type(s Scope) (Type, error) { + f, ok := s.LookupFunc(n.Func) + if !ok { + return TypeInvalid, fmt.Errorf("unknown function: %s", n.Func) + } + + return f.ReturnType, nil +} diff --git a/config/lang/ast/call_test.go b/config/lang/ast/call_test.go new file mode 100644 index 0000000000..ef63888d27 --- /dev/null +++ b/config/lang/ast/call_test.go @@ -0,0 +1,36 @@ +package ast + +import ( + "testing" +) + +func TestCallType(t *testing.T) { + c := &Call{Func: "foo"} + scope := &BasicScope{ + FuncMap: map[string]Function{ + "foo": Function{ReturnType: TypeString}, + }, + } + + actual, err := c.Type(scope) + if err != nil { + t.Fatalf("err: %s", err) + } + if actual != TypeString { + t.Fatalf("bad: %s", actual) + } +} + +func TestCallType_invalid(t *testing.T) { + c := &Call{Func: "bar"} + scope := &BasicScope{ + FuncMap: map[string]Function{ + "foo": Function{ReturnType: TypeString}, + }, + } + + _, err := c.Type(scope) + if err == nil { + t.Fatal("should error") + } +} diff --git a/config/lang/ast/concat.go b/config/lang/ast/concat.go index 871b0f44af..0246a3bc11 100644 --- a/config/lang/ast/concat.go +++ b/config/lang/ast/concat.go @@ -36,3 +36,7 @@ func (n *Concat) String() string { return b.String() } + +func (n *Concat) Type(Scope) (Type, error) { + return TypeString, nil +} diff --git a/config/lang/ast/concat_test.go b/config/lang/ast/concat_test.go new file mode 100644 index 0000000000..65fa676010 --- /dev/null +++ b/config/lang/ast/concat_test.go @@ -0,0 +1,16 @@ +package ast + +import ( + "testing" +) + +func TestConcatType(t *testing.T) { + c := &Concat{} + actual, err := c.Type(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + if actual != TypeString { + t.Fatalf("bad: %s", actual) + } +} diff --git a/config/lang/ast/literal.go b/config/lang/ast/literal.go index b314fcc216..9da3ff3a39 100644 --- a/config/lang/ast/literal.go +++ b/config/lang/ast/literal.go @@ -8,7 +8,7 @@ import ( // 42 or 3.14159. Based on the Type, the Value can be safely cast. type LiteralNode struct { Value interface{} - Type Type + Typex Type Posx Pos } @@ -27,3 +27,7 @@ func (n *LiteralNode) GoString() string { func (n *LiteralNode) String() string { return fmt.Sprintf("Literal(%s, %v)", n.Type, n.Value) } + +func (n *LiteralNode) Type(Scope) (Type, error) { + return n.Typex, nil +} diff --git a/config/lang/ast/literal_test.go b/config/lang/ast/literal_test.go new file mode 100644 index 0000000000..2759d77221 --- /dev/null +++ b/config/lang/ast/literal_test.go @@ -0,0 +1,16 @@ +package ast + +import ( + "testing" +) + +func TestLiteralNodeType(t *testing.T) { + c := &LiteralNode{Typex: TypeString} + actual, err := c.Type(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + if actual != TypeString { + t.Fatalf("bad: %s", actual) + } +} diff --git a/config/lang/ast/scope.go b/config/lang/ast/scope.go new file mode 100644 index 0000000000..77c3ee79ab --- /dev/null +++ b/config/lang/ast/scope.go @@ -0,0 +1,65 @@ +package ast + +// Scope is the interface used to look up variables and functions while +// evaluating. How these functions/variables are defined are up to the caller. +type Scope interface { + LookupFunc(string) (Function, bool) + LookupVar(string) (Variable, bool) +} + +// Variable is a variable value for execution given as input to the engine. +// It records the value of a variables along with their type. +type Variable struct { + Value interface{} + Type Type +} + +// Function defines a function that can be executed by the engine. +// The type checker will validate that the proper types will be called +// to the callback. +type Function struct { + // ArgTypes is the list of types in argument order. These are the + // required arguments. + // + // ReturnType is the type of the returned value. The Callback MUST + // return this type. + ArgTypes []Type + ReturnType Type + + // Variadic, if true, says that this function is variadic, meaning + // it takes a variable number of arguments. In this case, the + // VariadicType must be set. + Variadic bool + VariadicType Type + + // Callback is the function called for a function. The argument + // types are guaranteed to match the spec above by the type checker. + // The length of the args is strictly == len(ArgTypes) unless Varidiac + // is true, in which case its >= len(ArgTypes). + Callback func([]interface{}) (interface{}, error) +} + +// BasicScope is a simple scope that looks up variables and functions +// using a map. +type BasicScope struct { + FuncMap map[string]Function + VarMap map[string]Variable +} + +func (s *BasicScope) LookupFunc(n string) (Function, bool) { + if s == nil { + return Function{}, false + } + + v, ok := s.FuncMap[n] + return v, ok +} + +func (s *BasicScope) LookupVar(n string) (Variable, bool) { + if s == nil { + return Variable{}, false + } + + v, ok := s.VarMap[n] + return v, ok +} diff --git a/config/lang/ast/scope_test.go b/config/lang/ast/scope_test.go new file mode 100644 index 0000000000..b1484a1fdb --- /dev/null +++ b/config/lang/ast/scope_test.go @@ -0,0 +1,39 @@ +package ast + +import ( + "testing" +) + +func TestBasicScope_impl(t *testing.T) { + var _ Scope = new(BasicScope) +} + +func TestBasicScopeLookupFunc(t *testing.T) { + scope := &BasicScope{ + FuncMap: map[string]Function{ + "foo": Function{}, + }, + } + + if _, ok := scope.LookupFunc("bar"); ok { + t.Fatal("should not find bar") + } + if _, ok := scope.LookupFunc("foo"); !ok { + t.Fatal("should find foo") + } +} + +func TestBasicScopeLookupVar(t *testing.T) { + scope := &BasicScope{ + VarMap: map[string]Variable{ + "foo": Variable{}, + }, + } + + if _, ok := scope.LookupVar("bar"); ok { + t.Fatal("should not find bar") + } + if _, ok := scope.LookupVar("foo"); !ok { + t.Fatal("should find foo") + } +} diff --git a/config/lang/ast/stack.go b/config/lang/ast/stack.go new file mode 100644 index 0000000000..bd2bc15786 --- /dev/null +++ b/config/lang/ast/stack.go @@ -0,0 +1,25 @@ +package ast + +// Stack is a stack of Node. +type Stack struct { + stack []Node +} + +func (s *Stack) Len() int { + return len(s.stack) +} + +func (s *Stack) Push(n Node) { + s.stack = append(s.stack, n) +} + +func (s *Stack) Pop() Node { + x := s.stack[len(s.stack)-1] + s.stack[len(s.stack)-1] = nil + s.stack = s.stack[:len(s.stack)-1] + return x +} + +func (s *Stack) Reset() { + s.stack = nil +} diff --git a/config/lang/ast/stack_test.go b/config/lang/ast/stack_test.go new file mode 100644 index 0000000000..95a9d9255a --- /dev/null +++ b/config/lang/ast/stack_test.go @@ -0,0 +1,46 @@ +package ast + +import ( + "reflect" + "testing" +) + +func TestStack(t *testing.T) { + var s Stack + if s.Len() != 0 { + t.Fatalf("bad: %d", s.Len()) + } + + n := &LiteralNode{Value: 42} + s.Push(n) + + if s.Len() != 1 { + t.Fatalf("bad: %d", s.Len()) + } + + actual := s.Pop() + if !reflect.DeepEqual(actual, n) { + t.Fatalf("bad: %#v", actual) + } + + if s.Len() != 0 { + t.Fatalf("bad: %d", s.Len()) + } +} + +func TestStack_reset(t *testing.T) { + var s Stack + + n := &LiteralNode{Value: 42} + s.Push(n) + + if s.Len() != 1 { + t.Fatalf("bad: %d", s.Len()) + } + + s.Reset() + + if s.Len() != 0 { + t.Fatalf("bad: %d", s.Len()) + } +} diff --git a/config/lang/ast/variable_access.go b/config/lang/ast/variable_access.go index 148094a6a3..4c1362d753 100644 --- a/config/lang/ast/variable_access.go +++ b/config/lang/ast/variable_access.go @@ -25,3 +25,12 @@ func (n *VariableAccess) GoString() string { func (n *VariableAccess) String() string { return fmt.Sprintf("Variable(%s)", n.Name) } + +func (n *VariableAccess) Type(s Scope) (Type, error) { + v, ok := s.LookupVar(n.Name) + if !ok { + return TypeInvalid, fmt.Errorf("unknown variable: %s", n.Name) + } + + return v.Type, nil +} diff --git a/config/lang/ast/variable_access_test.go b/config/lang/ast/variable_access_test.go new file mode 100644 index 0000000000..1880bc5142 --- /dev/null +++ b/config/lang/ast/variable_access_test.go @@ -0,0 +1,36 @@ +package ast + +import ( + "testing" +) + +func TestVariableAccessType(t *testing.T) { + c := &VariableAccess{Name: "foo"} + scope := &BasicScope{ + VarMap: map[string]Variable{ + "foo": Variable{Type: TypeString}, + }, + } + + actual, err := c.Type(scope) + if err != nil { + t.Fatalf("err: %s", err) + } + if actual != TypeString { + t.Fatalf("bad: %s", actual) + } +} + +func TestVariableAccessType_invalid(t *testing.T) { + c := &VariableAccess{Name: "bar"} + scope := &BasicScope{ + VarMap: map[string]Variable{ + "foo": Variable{Type: TypeString}, + }, + } + + _, err := c.Type(scope) + if err == nil { + t.Fatal("should error") + } +} diff --git a/config/lang/engine.go b/config/lang/engine.go index b18db0f390..9bcdb55630 100644 --- a/config/lang/engine.go +++ b/config/lang/engine.go @@ -227,38 +227,6 @@ type Scope struct { FuncMap map[string]Function } -// Variable is a variable value for execution given as input to the engine. -// It records the value of a variables along with their type. -type Variable struct { - Value interface{} - Type ast.Type -} - -// Function defines a function that can be executed by the engine. -// The type checker will validate that the proper types will be called -// to the callback. -type Function struct { - // ArgTypes is the list of types in argument order. These are the - // required arguments. - // - // ReturnType is the type of the returned value. The Callback MUST - // return this type. - ArgTypes []ast.Type - ReturnType ast.Type - - // Variadic, if true, says that this function is variadic, meaning - // it takes a variable number of arguments. In this case, the - // VariadicType must be set. - Variadic bool - VariadicType ast.Type - - // Callback is the function called for a function. The argument - // types are guaranteed to match the spec above by the type checker. - // The length of the args is strictly == len(ArgTypes) unless Varidiac - // is true, in which case its >= len(ArgTypes). - Callback func([]interface{}) (interface{}, error) -} - // LookupFunc will look up a variable by name. // TODO test func (s *Scope) LookupFunc(n string) (Function, bool) {