Merge pull request from d5/condexpr

Conditional Expression
This commit is contained in:
Daniel Kang 2019-01-27 18:17:42 -08:00 committed by GitHub
commit 93917a4645
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 205 additions and 1 deletions

30
compiler/ast/cond_expr.go Normal file
View file

@ -0,0 +1,30 @@
package ast
import (
"github.com/d5/tengo/compiler/source"
)
// CondExpr represents a ternary conditional expression.
type CondExpr struct {
Cond Expr
True Expr
False Expr
QuestionPos source.Pos
ColonPos source.Pos
}
func (e *CondExpr) exprNode() {}
// Pos returns the position of first character belonging to the node.
func (e *CondExpr) Pos() source.Pos {
return e.Cond.Pos()
}
// End returns the position of first character immediately after the node.
func (e *CondExpr) End() source.Pos {
return e.False.End()
}
func (e *CondExpr) String() string {
return "(" + e.Cond.String() + " ? " + e.True.String() + " : " + e.False.String() + ")"
}

View file

@ -480,6 +480,33 @@ func (c *Compiler) Compile(node ast.Node) error {
}
c.emit(OpImmutable)
case *ast.CondExpr:
if err := c.Compile(node.Cond); err != nil {
return err
}
// first jump placeholder
jumpPos1 := c.emit(OpJumpFalsy, 0)
if err := c.Compile(node.True); err != nil {
return err
}
// second jump placeholder
jumpPos2 := c.emit(OpJump, 0)
// update first jump offset
curPos := len(c.currentInstructions())
c.changeOperand(jumpPos1, curPos)
if err := c.Compile(node.False); err != nil {
return err
}
// update second jump offset
curPos = len(c.currentInstructions())
c.changeOperand(jumpPos2, curPos)
}
return nil

View file

@ -82,7 +82,14 @@ func (p *Parser) parseExpr() ast.Expr {
defer un(trace(p, "Expression"))
}
return p.parseBinaryExpr(token.LowestPrec + 1)
expr := p.parseBinaryExpr(token.LowestPrec + 1)
// ternary conditional expression
if p.token == token.Question {
return p.parseCondExpr(expr)
}
return expr
}
func (p *Parser) parseBinaryExpr(prec1 int) ast.Expr {
@ -101,6 +108,7 @@ func (p *Parser) parseBinaryExpr(prec1 int) ast.Expr {
pos := p.expect(op)
y := p.parseBinaryExpr(prec + 1)
x = &ast.BinaryExpr{
LHS: x,
RHS: y,
@ -110,6 +118,24 @@ func (p *Parser) parseBinaryExpr(prec1 int) ast.Expr {
}
}
func (p *Parser) parseCondExpr(cond ast.Expr) ast.Expr {
questionPos := p.expect(token.Question)
trueExpr := p.parseExpr()
colonPos := p.expect(token.Colon)
falseExpr := p.parseExpr()
return &ast.CondExpr{
Cond: cond,
True: trueExpr,
False: falseExpr,
QuestionPos: questionPos,
ColonPos: colonPos,
}
}
func (p *Parser) parseUnaryExpr() ast.Expr {
if p.trace {
defer un(trace(p, "UnaryExpression"))

View file

@ -0,0 +1,48 @@
package parser_test
import (
"testing"
"github.com/d5/tengo/compiler/ast"
)
func TestCondExpr(t *testing.T) {
expect(t, "a ? b : c", func(p pfn) []ast.Stmt {
return stmts(
exprStmt(
condExpr(
ident("a", p(1, 1)),
ident("b", p(1, 5)),
ident("c", p(1, 9)),
p(1, 3),
p(1, 7))))
})
expect(t, `a ?
b :
c`, func(p pfn) []ast.Stmt {
return stmts(
exprStmt(
condExpr(
ident("a", p(1, 1)),
ident("b", p(1, 5)),
ident("c", p(1, 9)),
p(1, 3),
p(1, 7))))
})
expectString(t, `a ? b : c`, "(a ? b : c)")
expectString(t, `a + b ? c - d : e * f`, "((a + b) ? (c - d) : (e * f))")
expectString(t, `a == b ? c + (d / e) : f ? g : h + i`, "((a == b) ? (c + ((d / e))) : (f ? g : (h + i)))")
expectString(t, `(a + b) ? (c - d) : (e * f)`, "(((a + b)) ? ((c - d)) : ((e * f)))")
expectString(t, `a + (b ? c : d) - e`, "((a + ((b ? c : d))) - e)")
expectString(t, `a ? b ? c : d : e`, "(a ? (b ? c : d) : e)")
expectString(t, `a := b ? c : d`, "a := (b ? c : d)")
expectString(t, `x := a ? b ? c : d : e`, "x := (a ? (b ? c : d) : e)")
// ? : should be at the end of each line if it's multi-line
expectError(t, `a
? b
: c`)
expectError(t, `a ? (b : e)`)
expectError(t, `(a ? b) : e`)
}

View file

@ -184,6 +184,10 @@ func binaryExpr(x, y ast.Expr, op token.Token, pos source.Pos) *ast.BinaryExpr {
return &ast.BinaryExpr{LHS: x, RHS: y, Token: op, TokenPos: pos}
}
func condExpr(cond, trueExpr, falseExpr ast.Expr, questionPos, colonPos source.Pos) *ast.CondExpr {
return &ast.CondExpr{Cond: cond, True: trueExpr, False: falseExpr, QuestionPos: questionPos, ColonPos: colonPos}
}
func unaryExpr(x ast.Expr, op token.Token, pos source.Pos) *ast.UnaryExpr {
return &ast.UnaryExpr{Expr: x, Token: op, TokenPos: pos}
}
@ -398,6 +402,12 @@ func equalExpr(t *testing.T, expected, actual ast.Expr) bool {
assert.Equal(t, int(expected.ErrorPos), int(actual.(*ast.ErrorExpr).ErrorPos)) &&
assert.Equal(t, int(expected.LParen), int(actual.(*ast.ErrorExpr).LParen)) &&
assert.Equal(t, int(expected.RParen), int(actual.(*ast.ErrorExpr).RParen))
case *ast.CondExpr:
return equalExpr(t, expected.Cond, actual.(*ast.CondExpr).Cond) &&
equalExpr(t, expected.True, actual.(*ast.CondExpr).True) &&
equalExpr(t, expected.False, actual.(*ast.CondExpr).False) &&
assert.Equal(t, expected.QuestionPos, actual.(*ast.CondExpr).QuestionPos) &&
assert.Equal(t, expected.ColonPos, actual.(*ast.CondExpr).ColonPos)
default:
panic(fmt.Errorf("unknown type: %T", expected))
}

View file

@ -125,6 +125,8 @@ func (s *Scanner) Scan() (tok token.Token, literal string, pos source.Pos) {
}
case ',':
tok = token.Comma
case '?':
tok = token.Question
case ';':
tok = token.Semicolon
literal = ";"

View file

@ -64,6 +64,7 @@ const (
RBrace // }
Semicolon // ;
Colon // :
Question // ?
_operatorEnd
_keywordBeg
Break
@ -142,6 +143,7 @@ var tokens = [...]string{
RBrace: "}",
Semicolon: ";",
Colon: ":",
Question: "?",
Break: "break",
Case: "case",
Continue: "continue",

View file

@ -94,6 +94,21 @@ d := "hello world"[2:10] // == "llo worl"
```
> [Run in Playground](https://tengolang.com/?s=214ab490bb24549578770984985f6b161aed915d)
## Conditional Expression
Tengo supports the ternary conditional expression (`cond ? expr1 : expr2`):
```golang
a := true ? 1 : -1 // a == 1
min := func(a, b) {
return a < b ? a : b
}
b := min(5, 10) // b == 5
```
> [Run in Playground](https://tengolang.com/?s=24724cc03e8ef2e56b7851017f8c2a577897961e)
## Functions
In Tengo, functions are first-class citizen, and, it also supports closures, functions that captures variables in outer scopes. In the following example, the function returned from `adder` is capturing `base` variable.

44
runtime/vm_cond_test.go Normal file
View file

@ -0,0 +1,44 @@
package runtime_test
import "testing"
func TestCondExpr(t *testing.T) {
expect(t, `out = true ? 5 : 10`, 5)
expect(t, `out = false ? 5 : 10`, 10)
expect(t, `out = (1 == 1) ? 2 + 3 : 12 - 2`, 5)
expect(t, `out = (1 != 1) ? 2 + 3 : 12 - 2`, 10)
expect(t, `out = (1 == 1) ? true ? 10 - 8 : 1 + 3 : 12 - 2`, 2)
expect(t, `out = (1 == 1) ? false ? 10 - 8 : 1 + 3 : 12 - 2`, 4)
expect(t, `
f1 := func() { out += 10 }
f2 := func() { out = -out }
true ? f1() : f2()
`, 10)
expect(t, `
out = 5
f1 := func() { out += 10 }
f2 := func() { out = -out }
false ? f1() : f2()
`, -5)
expect(t, `
f1 := func(a) { return a + 2 }
f2 := func(a) { return a - 2 }
f3 := func(a) { return a + 10 }
f4 := func(a) { return -a }
f := func(c) {
return c == 0 ? f1(c) : f2(c) ? f3(c) : f4(c)
}
out = [f(0), f(1), f(2)]
`, ARR{2, 11, -2})
expect(t, `f := func(a) { return -a }; out = f(true ? 5 : 3)`, -5)
expect(t, `out = [false?5:10, true?1:2]`, ARR{10, 1})
expect(t, `
out = 1 > 2 ?
1 + 2 + 3 :
10 - 5`, 5)
}