diff --git a/config/lang/ast/arithmetic.go b/config/lang/ast/arithmetic.go new file mode 100644 index 000000000..94dc24f89 --- /dev/null +++ b/config/lang/ast/arithmetic.go @@ -0,0 +1,43 @@ +package ast + +import ( + "bytes" + "fmt" +) + +// Arithmetic represents a node where the result is arithmetic of +// two or more operands in the order given. +type Arithmetic struct { + Op ArithmeticOp + Exprs []Node + Posx Pos +} + +func (n *Arithmetic) Accept(v Visitor) Node { + for i, expr := range n.Exprs { + n.Exprs[i] = expr.Accept(v) + } + + return v(n) +} + +func (n *Arithmetic) Pos() Pos { + return n.Posx +} + +func (n *Arithmetic) GoString() string { + return fmt.Sprintf("*%#v", *n) +} + +func (n *Arithmetic) String() string { + var b bytes.Buffer + for _, expr := range n.Exprs { + b.WriteString(fmt.Sprintf("%s", expr)) + } + + return b.String() +} + +func (n *Arithmetic) Type(Scope) (Type, error) { + return TypeInt, nil +} diff --git a/config/lang/ast/arithmetic_op.go b/config/lang/ast/arithmetic_op.go new file mode 100644 index 000000000..920b1d56e --- /dev/null +++ b/config/lang/ast/arithmetic_op.go @@ -0,0 +1,12 @@ +package ast + +// ArithmeticOp is the operation to use for the math. +type ArithmeticOp int + +const ( + ArithmeticOpInvalid ArithmeticOp = 0 + ArithmeticOpAdd ArithmeticOp = iota + ArithmeticOpSub + ArithmeticOpMul + ArithmeticOpDiv +) diff --git a/config/lang/check_types.go b/config/lang/check_types.go index 0165eadfc..c153055de 100644 --- a/config/lang/check_types.go +++ b/config/lang/check_types.go @@ -55,6 +55,9 @@ func (v *TypeCheck) visit(raw ast.Node) ast.Node { var result ast.Node var err error switch n := raw.(type) { + case *ast.Arithmetic: + tc := &typeCheckArithmetic{n} + result, err = tc.TypeCheck(v) case *ast.Call: tc := &typeCheckCall{n} result, err = tc.TypeCheck(v) @@ -70,7 +73,7 @@ func (v *TypeCheck) visit(raw ast.Node) ast.Node { default: tc, ok := raw.(TypeCheckNode) if !ok { - err = fmt.Errorf("unknown node: %#v", raw) + err = fmt.Errorf("unknown node for type check: %#v", raw) break } @@ -86,6 +89,51 @@ func (v *TypeCheck) visit(raw ast.Node) ast.Node { return result } +type typeCheckArithmetic struct { + n *ast.Arithmetic +} + +func (tc *typeCheckArithmetic) TypeCheck(v *TypeCheck) (ast.Node, error) { + // The arguments are on the stack in reverse order, so pop them off. + args := make([]ast.Type, len(tc.n.Exprs)) + for i, _ := range tc.n.Exprs { + args[len(tc.n.Exprs)-1-i] = v.StackPop() + } + + // Determine the resulting type we want + mathType := ast.TypeInt + switch v := args[0]; v { + case ast.TypeInt: + fallthrough + case ast.TypeFloat: + mathType = v + default: + return nil, fmt.Errorf( + "Math operations can only be done with ints and floats, got %s", + v) + } + + // Verify the args + for i, arg := range args { + if arg != mathType { + cn := v.ImplicitConversion(args[i], mathType, tc.n.Exprs[i]) + if cn != nil { + tc.n.Exprs[i] = cn + continue + } + + return nil, fmt.Errorf( + "operand %d should be %s, got %s", + i+1, mathType, arg) + } + } + + // Return type + v.StackPush(mathType) + + return tc.n, nil +} + type typeCheckCall struct { n *ast.Call } diff --git a/config/lang/eval.go b/config/lang/eval.go index cc4d756df..d66563c27 100644 --- a/config/lang/eval.go +++ b/config/lang/eval.go @@ -134,6 +134,8 @@ func (v *evalVisitor) visit(raw ast.Node) ast.Node { // types as well as any other EvalNode implementations. func evalNode(raw ast.Node) (EvalNode, error) { switch n := raw.(type) { + case *ast.Arithmetic: + return &evalArithmetic{n}, nil case *ast.Call: return &evalCall{n}, nil case *ast.Concat: @@ -152,6 +154,69 @@ func evalNode(raw ast.Node) (EvalNode, error) { } } +type evalArithmetic struct{ *ast.Arithmetic } + +func (v *evalArithmetic) Eval( + s ast.Scope, stack *ast.Stack) (interface{}, ast.Type, error) { + // The arguments are on the stack in reverse order, so pop them off. + var resultType ast.Type + exprs := make([]interface{}, len(v.Exprs)) + for i, _ := range v.Exprs { + node := stack.Pop().(*ast.LiteralNode) + exprs[len(v.Exprs)-1-i] = node.Value + resultType = node.Typex + } + + // Do the operation + var result interface{} + var err error + switch resultType { + case ast.TypeInt: + result, err = v.evalInt(v.Op, exprs) + default: + return nil, resultType, fmt.Errorf( + "unknown math operand type: %s", resultType) + } + + return result, resultType, err +} + +func (v *evalArithmetic) evalInt( + op ast.ArithmeticOp, exprs []interface{}) (int, error) { + switch v.Op { + case ast.ArithmeticOpAdd: + result := 0 + for _, expr := range exprs { + result += expr.(int) + } + + return result, nil + case ast.ArithmeticOpSub: + result := exprs[0].(int) + for _, expr := range exprs[1:] { + result -= expr.(int) + } + + return result, nil + case ast.ArithmeticOpMul: + result := exprs[0].(int) + for _, expr := range exprs[1:] { + result *= expr.(int) + } + + return result, nil + case ast.ArithmeticOpDiv: + result := exprs[0].(int) + for _, expr := range exprs[1:] { + result /= expr.(int) + } + + return result, nil + default: + return 0, fmt.Errorf("unknown math operation: %s", v.Op) + } +} + type evalCall struct{ *ast.Call } func (v *evalCall) Eval(s ast.Scope, stack *ast.Stack) (interface{}, ast.Type, error) { diff --git a/config/lang/eval_test.go b/config/lang/eval_test.go index bf66be79f..e0c53db83 100644 --- a/config/lang/eval_test.go +++ b/config/lang/eval_test.go @@ -39,6 +39,38 @@ func TestEval(t *testing.T) { ast.TypeString, }, + { + "foo ${42+1}", + nil, + false, + "foo 43", + ast.TypeString, + }, + + { + "foo ${42-1}", + nil, + false, + "foo 41", + ast.TypeString, + }, + + { + "foo ${42*2}", + nil, + false, + "foo 84", + ast.TypeString, + }, + + { + "foo ${42/2}", + nil, + false, + "foo 21", + ast.TypeString, + }, + { "foo ${rand()}", &ast.BasicScope{ diff --git a/config/lang/lang.y b/config/lang/lang.y index 4ee47fb54..68c7d0b37 100644 --- a/config/lang/lang.y +++ b/config/lang/lang.y @@ -22,9 +22,9 @@ import ( %token PROGRAM_STRING_START PROGRAM_STRING_END %token PAREN_LEFT PAREN_RIGHT COMMA -%token IDENTIFIER INTEGER FLOAT STRING +%token ARITH_OP IDENTIFIER INTEGER FLOAT STRING -%type expr interpolation literal literalModeTop literalModeValue +%type arith expr interpolation literal literalModeTop literalModeValue %type args %% @@ -116,6 +116,10 @@ expr: Posx: $1.Pos, } } +| arith + { + $$ = $1 + } | IDENTIFIER { $$ = &ast.VariableAccess{Name: $1.Value.(string), Posx: $1.Pos} @@ -125,6 +129,27 @@ expr: $$ = &ast.Call{Func: $1.Value.(string), Args: $3, Posx: $1.Pos} } +arith: + INTEGER ARITH_OP INTEGER + { + $$ = &ast.Arithmetic{ + Op: $2.Value.(ast.ArithmeticOp), + Exprs: []ast.Node{ + &ast.LiteralNode{ + Value: $1.Value.(int), + Typex: ast.TypeInt, + Posx: $1.Pos, + }, + &ast.LiteralNode{ + Value: $3.Value.(int), + Typex: ast.TypeInt, + Posx: $3.Pos, + }, + }, + Posx: $1.Pos, + } + } + args: { $$ = nil diff --git a/config/lang/lex.go b/config/lang/lex.go index a1172b420..6b30f2a4c 100644 --- a/config/lang/lex.go +++ b/config/lang/lex.go @@ -168,6 +168,18 @@ func (x *parserLex) lexModeInterpolation(yylval *parserSymType) int { return PAREN_RIGHT case ',': return COMMA + case '+': + yylval.token = &parserToken{Value: ast.ArithmeticOpAdd} + return ARITH_OP + case '-': + yylval.token = &parserToken{Value: ast.ArithmeticOpSub} + return ARITH_OP + case '*': + yylval.token = &parserToken{Value: ast.ArithmeticOpMul} + return ARITH_OP + case '/': + yylval.token = &parserToken{Value: ast.ArithmeticOpDiv} + return ARITH_OP default: x.backup() return x.lexId(yylval) diff --git a/config/lang/lex_test.go b/config/lang/lex_test.go index 8c5207d78..5341e594a 100644 --- a/config/lang/lex_test.go +++ b/config/lang/lex_test.go @@ -63,6 +63,15 @@ func TestLex(t *testing.T) { PROGRAM_BRACKET_RIGHT, lexEOF}, }, + { + "${bar(42+1)}", + []int{PROGRAM_BRACKET_LEFT, + IDENTIFIER, PAREN_LEFT, + INTEGER, ARITH_OP, INTEGER, + PAREN_RIGHT, + PROGRAM_BRACKET_RIGHT, lexEOF}, + }, + { "${bar(3.14159)}", []int{PROGRAM_BRACKET_LEFT, diff --git a/config/lang/parse_test.go b/config/lang/parse_test.go index e2ba2b310..d2671e233 100644 --- a/config/lang/parse_test.go +++ b/config/lang/parse_test.go @@ -152,6 +152,37 @@ func TestParse(t *testing.T) { }, }, + { + "foo ${42+1}", + false, + &ast.Concat{ + Posx: ast.Pos{Column: 1, Line: 1}, + Exprs: []ast.Node{ + &ast.LiteralNode{ + Value: "foo ", + Typex: ast.TypeString, + Posx: ast.Pos{Column: 1, Line: 1}, + }, + &ast.Arithmetic{ + Op: ast.ArithmeticOpAdd, + Exprs: []ast.Node{ + &ast.LiteralNode{ + Value: 42, + Typex: ast.TypeInt, + Posx: ast.Pos{Column: 7, Line: 1}, + }, + &ast.LiteralNode{ + Value: 1, + Typex: ast.TypeInt, + Posx: ast.Pos{Column: 10, Line: 1}, + }, + }, + Posx: ast.Pos{Column: 7, Line: 1}, + }, + }, + }, + }, + { "${foo()}", false, diff --git a/config/lang/y.go b/config/lang/y.go index de1ce7501..bb56ebdc0 100644 --- a/config/lang/y.go +++ b/config/lang/y.go @@ -24,10 +24,11 @@ const PROGRAM_STRING_END = 57349 const PAREN_LEFT = 57350 const PAREN_RIGHT = 57351 const COMMA = 57352 -const IDENTIFIER = 57353 -const INTEGER = 57354 -const FLOAT = 57355 -const STRING = 57356 +const ARITH_OP = 57353 +const IDENTIFIER = 57354 +const INTEGER = 57355 +const FLOAT = 57356 +const STRING = 57357 var parserToknames = []string{ "PROGRAM_BRACKET_LEFT", @@ -37,6 +38,7 @@ var parserToknames = []string{ "PAREN_LEFT", "PAREN_RIGHT", "COMMA", + "ARITH_OP", "IDENTIFIER", "INTEGER", "FLOAT", @@ -48,7 +50,7 @@ const parserEofCode = 1 const parserErrCode = 2 const parserMaxDepth = 200 -//line lang.y:151 +//line lang.y:176 //line yacctab:1 var parserExca = []int{ @@ -57,51 +59,51 @@ var parserExca = []int{ -2, 0, } -const parserNprod = 17 +const parserNprod = 19 const parserPrivate = 57344 var parserTokenNames []string var parserStates []string -const parserLast = 23 +const parserLast = 26 var parserAct = []int{ - 9, 7, 7, 3, 18, 19, 8, 15, 13, 11, - 12, 6, 6, 14, 8, 1, 17, 10, 2, 16, - 20, 4, 5, + 9, 7, 7, 18, 3, 21, 22, 8, 17, 14, + 11, 12, 6, 6, 16, 8, 15, 1, 20, 10, + 2, 19, 4, 23, 5, 13, } var parserPact = []int{ - -2, -1000, -2, -1000, -1000, -1000, -1000, -3, -1000, 8, - -2, -1000, -1000, -1, -1000, -3, -5, -1000, -1000, -3, - -1000, + -2, -1000, -2, -1000, -1000, -1000, -1000, -3, -1000, 11, + -2, 3, -1000, -1000, 0, -1000, -10, -3, -1000, -4, + -1000, -1000, -3, -1000, } var parserPgo = []int{ - 0, 0, 22, 21, 17, 3, 19, 15, + 0, 25, 0, 24, 22, 19, 4, 21, 17, } var parserR1 = []int{ - 0, 7, 7, 4, 4, 5, 5, 2, 1, 1, - 1, 1, 1, 6, 6, 6, 3, + 0, 8, 8, 5, 5, 6, 6, 3, 2, 2, + 2, 2, 2, 2, 1, 7, 7, 7, 4, } var parserR2 = []int{ 0, 0, 1, 1, 2, 1, 1, 3, 1, 1, - 1, 1, 4, 0, 3, 1, 1, + 1, 1, 1, 4, 3, 0, 3, 1, 1, } var parserChk = []int{ - -1000, -7, -4, -5, -3, -2, 14, 4, -5, -1, - -4, 12, 13, 11, 5, 8, -6, -1, 9, 10, - -1, + -1000, -8, -5, -6, -4, -3, 15, 4, -6, -2, + -5, 13, 14, -1, 12, 5, 11, 8, 13, -7, + -2, 9, 10, -2, } var parserDef = []int{ - 1, -2, 2, 3, 5, 6, 16, 0, 4, 0, - 8, 9, 10, 11, 7, 13, 0, 15, 12, 0, - 14, + 1, -2, 2, 3, 5, 6, 18, 0, 4, 0, + 8, 9, 10, 11, 12, 7, 0, 15, 14, 0, + 17, 13, 0, 16, } var parserTok1 = []int{ @@ -110,7 +112,7 @@ var parserTok1 = []int{ var parserTok2 = []int{ 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, - 12, 13, 14, + 12, 13, 14, 15, } var parserTok3 = []int{ 0, @@ -433,30 +435,55 @@ parserdefault: case 11: //line lang.y:120 { - parserVAL.node = &ast.VariableAccess{Name: parserS[parserpt-0].token.Value.(string), Posx: parserS[parserpt-0].token.Pos} + parserVAL.node = parserS[parserpt-0].node } case 12: //line lang.y:124 { - parserVAL.node = &ast.Call{Func: parserS[parserpt-3].token.Value.(string), Args: parserS[parserpt-1].nodeList, Posx: parserS[parserpt-3].token.Pos} + parserVAL.node = &ast.VariableAccess{Name: parserS[parserpt-0].token.Value.(string), Posx: parserS[parserpt-0].token.Pos} } case 13: - //line lang.y:129 + //line lang.y:128 + { + parserVAL.node = &ast.Call{Func: parserS[parserpt-3].token.Value.(string), Args: parserS[parserpt-1].nodeList, Posx: parserS[parserpt-3].token.Pos} + } + case 14: + //line lang.y:134 + { + parserVAL.node = &ast.Arithmetic{ + Op: parserS[parserpt-1].token.Value.(ast.ArithmeticOp), + Exprs: []ast.Node{ + &ast.LiteralNode{ + Value: parserS[parserpt-2].token.Value.(int), + Typex: ast.TypeInt, + Posx: parserS[parserpt-2].token.Pos, + }, + &ast.LiteralNode{ + Value: parserS[parserpt-0].token.Value.(int), + Typex: ast.TypeInt, + Posx: parserS[parserpt-0].token.Pos, + }, + }, + Posx: parserS[parserpt-2].token.Pos, + } + } + case 15: + //line lang.y:154 { parserVAL.nodeList = nil } - case 14: - //line lang.y:133 + case 16: + //line lang.y:158 { parserVAL.nodeList = append(parserS[parserpt-2].nodeList, parserS[parserpt-0].node) } - case 15: - //line lang.y:137 + case 17: + //line lang.y:162 { parserVAL.nodeList = append(parserVAL.nodeList, parserS[parserpt-0].node) } - case 16: - //line lang.y:143 + case 18: + //line lang.y:168 { parserVAL.node = &ast.LiteralNode{ Value: parserS[parserpt-0].token.Value.(string), diff --git a/config/lang/y.output b/config/lang/y.output index aa076e45a..c1f9df4a0 100644 --- a/config/lang/y.output +++ b/config/lang/y.output @@ -51,21 +51,22 @@ state 5 state 6 - literal: STRING. (16) + literal: STRING. (18) - . reduce 16 (src line 141) + . reduce 18 (src line 166) state 7 interpolation: PROGRAM_BRACKET_LEFT.expr PROGRAM_BRACKET_RIGHT PROGRAM_BRACKET_LEFT shift 7 - IDENTIFIER shift 13 + IDENTIFIER shift 14 INTEGER shift 11 FLOAT shift 12 STRING shift 6 . error + arith goto 13 expr goto 9 interpolation goto 5 literal goto 4 @@ -81,7 +82,7 @@ state 8 state 9 interpolation: PROGRAM_BRACKET_LEFT expr.PROGRAM_BRACKET_RIGHT - PROGRAM_BRACKET_RIGHT shift 14 + PROGRAM_BRACKET_RIGHT shift 15 . error @@ -99,7 +100,9 @@ state 10 state 11 expr: INTEGER. (9) + arith: INTEGER.ARITH_OP INTEGER + ARITH_OP shift 16 . reduce 9 (src line 103) @@ -110,89 +113,110 @@ state 12 state 13 - expr: IDENTIFIER. (11) - expr: IDENTIFIER.PAREN_LEFT args PAREN_RIGHT + expr: arith. (11) - PAREN_LEFT shift 15 . reduce 11 (src line 119) state 14 + expr: IDENTIFIER. (12) + expr: IDENTIFIER.PAREN_LEFT args PAREN_RIGHT + + PAREN_LEFT shift 17 + . reduce 12 (src line 123) + + +state 15 interpolation: PROGRAM_BRACKET_LEFT expr PROGRAM_BRACKET_RIGHT. (7) . reduce 7 (src line 92) -state 15 - expr: IDENTIFIER PAREN_LEFT.args PAREN_RIGHT - args: . (13) - - PROGRAM_BRACKET_LEFT shift 7 - IDENTIFIER shift 13 - INTEGER shift 11 - FLOAT shift 12 - STRING shift 6 - . reduce 13 (src line 128) - - expr goto 17 - interpolation goto 5 - literal goto 4 - literalModeTop goto 10 - literalModeValue goto 3 - args goto 16 - state 16 - expr: IDENTIFIER PAREN_LEFT args.PAREN_RIGHT - args: args.COMMA expr + arith: INTEGER ARITH_OP.INTEGER - PAREN_RIGHT shift 18 - COMMA shift 19 + INTEGER shift 18 . error state 17 - args: expr. (15) - - . reduce 15 (src line 136) - - -state 18 - expr: IDENTIFIER PAREN_LEFT args PAREN_RIGHT. (12) - - . reduce 12 (src line 123) - - -state 19 - args: args COMMA.expr + expr: IDENTIFIER PAREN_LEFT.args PAREN_RIGHT + args: . (15) PROGRAM_BRACKET_LEFT shift 7 - IDENTIFIER shift 13 + IDENTIFIER shift 14 INTEGER shift 11 FLOAT shift 12 STRING shift 6 - . error + . reduce 15 (src line 153) + arith goto 13 expr goto 20 interpolation goto 5 literal goto 4 literalModeTop goto 10 literalModeValue goto 3 + args goto 19 -state 20 - args: args COMMA expr. (14) +state 18 + arith: INTEGER ARITH_OP INTEGER. (14) . reduce 14 (src line 132) -14 terminals, 8 nonterminals -17 grammar rules, 21/2000 states +state 19 + expr: IDENTIFIER PAREN_LEFT args.PAREN_RIGHT + args: args.COMMA expr + + PAREN_RIGHT shift 21 + COMMA shift 22 + . error + + +state 20 + args: expr. (17) + + . reduce 17 (src line 161) + + +state 21 + expr: IDENTIFIER PAREN_LEFT args PAREN_RIGHT. (13) + + . reduce 13 (src line 127) + + +state 22 + args: args COMMA.expr + + PROGRAM_BRACKET_LEFT shift 7 + IDENTIFIER shift 14 + INTEGER shift 11 + FLOAT shift 12 + STRING shift 6 + . error + + arith goto 13 + expr goto 23 + interpolation goto 5 + literal goto 4 + literalModeTop goto 10 + literalModeValue goto 3 + +state 23 + args: args COMMA expr. (16) + + . reduce 16 (src line 157) + + +15 terminals, 9 nonterminals +19 grammar rules, 24/2000 states 0 shift/reduce, 0 reduce/reduce conflicts reported -57 working sets used -memory: parser 25/30000 -16 extra closures -25 shift entries, 1 exceptions -12 goto entries -15 entries saved by goto default -Optimizer space used: output 23/30000 -23 table entries, 0 zero -maximum spread: 14, maximum offset: 19 +58 working sets used +memory: parser 28/30000 +19 extra closures +27 shift entries, 1 exceptions +13 goto entries +17 entries saved by goto default +Optimizer space used: output 26/30000 +26 table entries, 0 zero +maximum spread: 15, maximum offset: 22