Skip to content

Commit

Permalink
Allow a comma-list of values per cond case (#196)
Browse files Browse the repository at this point in the history
Changes proposed in this pull request to support following syntax:
- `cond ((1 > 0 && 3 > 2): 1, (2 > 3) || (1 < 0): 2, *:1 + 2)`
- `let a = cond (cond (1 > 2 : 1, * : 11) < 2 : 1, 2 < 3: 2, *:1 + 2);a * 10`
- `let a = 2; a cond (1 :1 + 10, (2,3) : 2, *:1 + 2)`
- `let a = 2;
	a cond (
		1:"lo",
		(2,3): "med",
		*: "hi")`
- `let a = 2; a cond ( a cond ((1,2) : 1): "A", (2, 3): "B", *: "C")`
- `let a = 1; a cond ( cond (2 > 1 : 1): "A", (2, 3): "B", *: "C")`
- `let a = 1; cond ( a cond (1 : 1) : "A", 2: "B", *: "C")`
  • Loading branch information
ericzhang6222 authored May 11, 2020
1 parent 756019b commit 2f40b95
Show file tree
Hide file tree
Showing 10 changed files with 207 additions and 35 deletions.
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
syntax/parser.go linguist-generated=true

63 changes: 61 additions & 2 deletions cmd/arrai/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,18 @@ func TestEvalCond(t *testing.T) {
assertEvalOutputs(t, `3`, `cond (1 > 2 : 1, 2 > 3: 2, 3 < 4 :3, 4 < 5 : 5, 5 > 6 : 6, *:2 + 2)`)
assertEvalOutputs(t, `2`, `cond (1 > 2 : 1, 2 < 3: 2, 3 < 4 :3, 4 < 5 : 5, 5 > 6 : 6, *:2 + 2)`)

// Nested call
assertEvalOutputs(t, `1`, `cond (cond (1 > 0 : 1) > 0 : 1, 2 < 3: 2, *:1 + 2)`)
assertEvalOutputs(t, `2`, `cond (cond (1 > 2 : 1, * : 11) < 2 : 1, 2 < 3: 2, *:1 + 2)`)
assertEvalOutputs(t, `20`, `let a = cond (cond (1 > 2 : 1, * : 11) < 2 : 1, 2 < 3: 2, *:1 + 2);a * 10`)

var sb strings.Builder
assert.Error(t, evalImpl(`cond (1 < 0 : 1, 2 > 3: 2)`, &sb))
assert.Error(t, evalImpl(`cond (1 < 0 : 1)`, &sb))
assert.Error(t, evalImpl(`cond ()`, &sb))
}

func TestEvalCondExpr(t *testing.T) {
func TestEvalCondStr(t *testing.T) {
t.Parallel()
assertEvalExprString(t, "((1>0):1,(2>3):2,*:(1+2))", "cond (1 > 0 : 1, 2 > 3: 2, *:1 + 2,)")
assertEvalExprString(t, "((1>0):1,(2>3):2,*:(1+2))", "cond (1 > 0 : 1, 2 > 3: 2, *:1 + 2)")
Expand All @@ -75,6 +80,33 @@ func TestEvalCondExpr(t *testing.T) {
assertEvalExprString(t, "((1<2):1,*:(1+2))", "cond (1 < 2: 1, * : 1 + 2)")
}

// TestEvalCondMulti executes the cases whose condition has multiple expressions.
func TestEvalCondMulti(t *testing.T) {
t.Parallel()
assertEvalOutputs(t, `1`, `cond (1 > 0 || 3 > 2: 1, 2 > 3: 2, *:1 + 2,)`)
assertEvalOutputs(t, `1`, `cond (0 > 1 || 3 > 2: 1, 2 > 3: 2, *:1 + 2,)`)
assertEvalOutputs(t, `3`, `cond (0 > 1 || 3 > 4: 1, 2 > 3: 2, *:1 + 2,)`)
assertEvalOutputs(t, `1`, `cond (1 > 0 && 3 > 2: 1, 2 > 3: 2, *:1 + 2,)`)
assertEvalOutputs(t, `1`, `cond ((1 > 0 && 3 > 2): 1, (2 > 3) || (1 < 0): 2, *:1 + 2,)`)
assertEvalOutputs(t, `3`, `let a = cond (1 > 2 && 2 > 1: 1, * : 1 + 2);a`)
// Multiple true conditions
assertEvalOutputs(t, `1`, `cond (1 > 0 && 3 > 2: 1, 2 > 1: 2, *:1 + 2,)`)
assertEvalOutputs(t, `2`, `cond ((1 > 0 && 3 < 2): 1, (2 > 1) || (1 > 0): 2, *:1 + 2,)`)
assertEvalOutputs(t, `2`, `let a = cond (1 > 2 && 2 > 1: 1, (2 > 1) : 2, * : 1 + 2);a`)

var sb strings.Builder
assert.Error(t, evalImpl(`cond (1 < 0 || 2 > 3 : 1, 2 > 3: 2)`, &sb))
assert.Error(t, evalImpl(`cond (1 < 0 || 3 > 4 : 1)`, &sb))
}

// TestEvalCondMultiStr executes the cases whose condition has multiple expressions.
func TestEvalCondMultiStr(t *testing.T) {
t.Parallel()
assertEvalExprString(t, "((control_var:1),(((1>0))&&((2>1)):1))", "(1) cond (1 > 0 && 2 > 1 : 1)")
assertEvalExprString(t, "((control_var:1),(((1>0))||((2>1)):1))", "(1) cond (1 > 0 || 2 > 1 : 1)")
assertEvalExprString(t, "((control_var:1),(((1>0))||((2>1)):1,*:11))", "(1) cond (1 > 0 || 2 > 1 : 1, * : 11)")
}

func TestEvalCondWithControlVar(t *testing.T) {
t.Parallel()
// Control var conditions
Expand All @@ -94,14 +126,18 @@ func TestEvalCondWithControlVar(t *testing.T) {
assertEvalOutputs(t, `3`, `let a = 1; (a + 10) cond (1 :1, 2 :2, *:1 + 2)`)
assertEvalOutputs(t, `2`, `let a = 1; let b = (a + 1) cond (1 :1, 2 :2, *:1 + 2); b`)
assertEvalOutputs(t, `300`, `let a = 1; let b = (a + 10) cond (1 :1, 2 :2, *:1 + 2); b * 100`)
// Nested call
assertEvalOutputs(t, "B", `let a = 2; a cond ( a cond ((1,2) : 1): "A", (2, 3): "B", *: "C")`)
assertEvalOutputs(t, "A", `let a = 1; a cond ( cond (2 > 1 : 1): "A", (2, 3): "B", *: "C")`)
assertEvalOutputs(t, "A", `let a = 1; cond ( a cond (1 : 1) : "A", 2: "B", *: "C")`)

var sb strings.Builder
assert.Error(t, evalImpl(`let a = 3; a cond (1 :1, 2 :2 + 1)`, &sb))
assert.Error(t, evalImpl(`let a = 3; let b = a cond (1 :1, 2 :2 + 1); b`, &sb))
assert.Error(t, evalImpl(`let a = 3; let b = (a + 10) cond (1 :1, 2 :2 + 1); b`, &sb))
}

func TestEvalCondWithControlVarExpr(t *testing.T) {
func TestEvalCondWithControlVarStr(t *testing.T) {
t.Parallel()
assertEvalExprString(t, "((control_var:1),(1:1))", "(1) cond (1 : 1)")
assertEvalExprString(t, "((control_var:1),(1:1))", "(1) cond (1 : 1,)")
Expand All @@ -119,3 +155,26 @@ func TestEvalCondWithControlVarExpr(t *testing.T) {
assertEvalExprString(t, "(3->(\\a((control_var:(a+2)),((1+2):1,*:(1+2)))))",
"let a = 3; (a + 2) cond (1 + 2: 1, * : 1 + 2)")
}

func TestEvalCondWithControlVarMulti(t *testing.T) {
assertEvalOutputs(t, `1`, `let a = 1; a cond ((1,2) :1)`)
assertEvalOutputs(t, `1`, `let a = 2; a cond ((1,2,3) :1, 2 :2)`)
assertEvalOutputs(t, `1`, `let a = 3; a cond ((1,2,3) :1, 2 :2, *:1 + 2)`)
assertEvalOutputs(t, `2`, `let a = 2; a cond (1 :1 + 10, (2,3) : 2, *:1 + 2)`)

assertEvalOutputs(t, `med`, `let a = 2;
a cond (
1:"lo",
(2,3): "med",
*: "hi")`)

var sb strings.Builder
assert.Error(t, evalImpl(`let a = 1; a cond ((2,3)) : 2, 3: 3)`, &sb))
assert.Error(t, evalImpl(`let a = 1; a cond ((2,3)) : 2, (3,5): 3)`, &sb))
}

func TestEvalCondWithControlVarMultiStr(t *testing.T) {
t.Parallel()
assertEvalExprString(t, "((control_var:1),([1,2]:1))", "(1) cond ((1,2) :1)")
assertEvalExprString(t, "((control_var:2),(1:(1+10),[2,3]:2,*:(1+2)))", "(2) cond (1 :1 + 10, (2,3) : 2, *:1 + 2)")
}
41 changes: 41 additions & 0 deletions docs/example.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,27 @@ $ arrai eval 'let a = cond ( 1 < 2 : 1, 2 > 3 : 2, * : 3);a * 3'
$ arrai eval 'let a = cond ( 2 < 1 : 1, 2 < 3 : 2, * : 3);a * 3'
6
```

```bash
$ arrai eval 'let a = cond ( 2 < 1 || 1 > 0 : 1, 2 < 3 : 2, * : 3);a * 3'
3
```

```bash
$ arrai eval 'let a = cond ( 2 < 1 || 1 > 2 : 1, 2 < 3 && 1 > 0 : 2, * : 3);a * 3'
6
```

```bash
$ arrai eval 'cond (cond (1 > 0 : 1) > 0 : 1, 2 < 3: 2, *:1 + 2)'
1
```

```bash
$ arrai eval 'cond (cond (1 > 2 : 1, * : 11) < 2 : 1, 2 < 3: 2, *:1 + 2)'
2
```

#### Control Var Cases
```bash
$ arrai eval 'let a = 1; a cond (1 :1, 2 :2, *:1 + 2)'
Expand All @@ -122,6 +143,26 @@ $ arrai eval 'let a = 1; (a + 1) cond (1 :1, 2 :2, *:1 + 2)'
2
```

```bash
$ arrai eval 'let a = 2; a cond ( 1: "A", (2, 3): "B", *: "C")'
B
```

```bash
$ arrai eval 'let a = 2; a cond ( a cond ((1,2) : 1): "A", (2, 3): "B", *: "C")'
B
```

```bash
$ arrai eval 'let a = 1; a cond ( cond (2 > 1 : 1): "A", (2, 3): "B", *: "C")'
A
```

```bash
$ arrai eval 'let a = 1; cond ( a cond (1 : 1) : "A", 2: "B", *: "C")'
A
```

<!-- TODO: Uncomment once this works again.
### Apply a transform to inbound data
Expand Down
12 changes: 8 additions & 4 deletions rel/expr_cond.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ import (
type CondExpr struct {
ExprScanner
dicExpr, defaultExpr Expr
validValidation func(condition Value, local Scope) bool // Valid condition validation.
validValidation func(condition Value, local Scope) (bool, error) // Valid condition validation.
}

// NewCondExpr returns a new CondExpr.
func NewCondExpr(scanner parser.Scanner, dict Expr, defaultExpr Expr) Expr {
return &CondExpr{ExprScanner{scanner}, dict, defaultExpr, func(condition Value, local Scope) bool {
return condition.IsTrue()
return &CondExpr{ExprScanner{scanner}, dict, defaultExpr, func(condition Value, local Scope) (bool, error) {
return condition.IsTrue(), nil
}}
}

Expand Down Expand Up @@ -64,7 +64,11 @@ func (e *CondExpr) Eval(local Scope) (Value, error) {
return nil, wrapContext(err, e)
}

if cond != nil && e.validValidation(cond, local) {
valid, err := e.validValidation(cond, local)
if err != nil {
return nil, wrapContext(err, e)
}
if cond != nil && valid {
trueCond = &tempExpr
break
}
Expand Down
28 changes: 24 additions & 4 deletions rel/expr_cond_control_var.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,32 @@ type CondControlVarExpr struct {
// NewCondControlVarExpr returns a new CondControlVarExpr.
func NewCondControlVarExpr(scanner parser.Scanner, controlVar Expr, dictExpr Expr, defaultExpr Expr) Expr {
return &CondControlVarExpr{ExprScanner{scanner}, controlVar,
CondExpr{ExprScanner{scanner}, dictExpr, defaultExpr, func(condition Value, local Scope) bool {
CondExpr{ExprScanner{scanner}, dictExpr, defaultExpr, func(condition Value, local Scope) (bool, error) {
controlVarVal, has := local.Get("controlVarVal")
if !has {
return false
return false, fmt.Errorf("couldn't get 'controlVarVal' in Scope, and it is expected")
}
return condition.Equal(controlVarVal)

// process "(1,2):11" in case arrai e "(2) cond ((1,2):11,2:12,*:13)"
switch condition := condition.(type) {
case Array:
varVal, err := controlVarVal.Eval(local)
if err != nil {
currentExpr, has := local.Get("currentExpr")
if !has {
return false, fmt.Errorf("%s", err.Error())
}
return false, wrapContext(err, currentExpr)
}
for _, exprVal := range condition.Values() {
if exprVal.Equal(varVal) {
return true, nil
}
}
return false, nil
}

return condition.Equal(controlVarVal), nil
}}}
}

Expand All @@ -49,6 +69,6 @@ func (e *CondControlVarExpr) Eval(local Scope) (Value, error) {
return nil, err
}

local = local.With("controlVarVal", controlVarVal)
local = local.With("controlVarVal", controlVarVal).With("currentExpr", e)
return e.standardExpr.Eval(local)
}
2 changes: 1 addition & 1 deletion syntax/arrai.wbnf
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ expr -> C* amp="&"* @ C* arrow=(
> C* @:binop="&&" C*
> C* @:compare=/{!?(?:<:|<>?=?|>=?|=)} C*
> C* @ if=("if" t=expr ("else" f=expr)?)* C*
> C* @ cond=("cond" "(" (key=(("(" expr:",", ")") | expr) ":" value=expr):",",? ("*" ":" f=expr ","?)? ")")? C*
> C* @:binop=/{\+\+|[+|]|-%?} C*
> C* @:binop=/{&~|&|~~?|[-<][-&][->]} C*
> C* @:binop=/{//|[*/%]|\\} C*
Expand All @@ -30,7 +31,6 @@ expr -> C* amp="&"* @ C* arrow=(
| C* "{" C* set=(elt=@:",",?) "}" C*
| C* "{" C* dict=((key=@ ":" value=@):",",?) "}" C*
| C* cond=("cond" "(" (key=@ ":" value=@):",",? ("*" ":" f=expr ","?)? ")") C*
| C* cond=(("(" control_var=expr ")" | IDENT)? C* "cond" "(" (key=@ ":" value=@):",",? ("*" ":" f=expr ","?)? ")") C*
| C* "[" C* array=(item=@:",",?) "]" C*
| C* "{:" C* embed=(grammar=@ ":" subgrammar=%%ast) ":}" C*
| C* op="\\\\" @ C*
Expand Down
43 changes: 26 additions & 17 deletions syntax/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,12 @@ func MustCompile(filepath, source string) rel.Expr {
}

func (pc ParseContext) CompileExpr(b ast.Branch) rel.Expr {
// Note: please make sure if it is necessary to add new syntax name before `expr`.
name, c := which(b,
"amp", "arrow", "let", "unop", "binop", "compare", "rbinop", "if", "get",
"tail", "count", "touch", "get", "rel", "set", "dict", "array",
"embed", "op", "fn", "pkg", "tuple", "xstr", "IDENT", "STR", "NUM",
"expr", "cond",
"embed", "op", "fn", "pkg", "tuple", "xstr", "IDENT", "STR", "NUM", "cond",
"expr",
)
if c == nil {
panic(fmt.Errorf("misshapen node AST: %v", b))
Expand All @@ -87,7 +88,7 @@ func (pc ParseContext) CompileExpr(b ast.Branch) rel.Expr {
case "if":
return pc.compileIf(b, c)
case "cond":
return pc.compileCond(c)
return pc.compileCond(b, c)
case "count", "touch":
return pc.compileCountTouch(b)
case "get", "tail":
Expand Down Expand Up @@ -252,10 +253,19 @@ func (pc ParseContext) compileIf(b ast.Branch, c ast.Children) rel.Expr {
return result
}

func (pc ParseContext) compileCond(c ast.Children) rel.Expr {
func (pc ParseContext) compileCond(b ast.Branch, c ast.Children) rel.Expr {
// arrai eval 'cond (1 > 0:1, 2 > 3:2, *:10)'
var result rel.Expr
entryExprs := pc.compileDictEntryExprs(c)

keys := c.(ast.One).Node.(ast.Branch)["key"]
values := c.(ast.One).Node.(ast.Branch)["value"]
var keyExprs, valueExprs []rel.Expr
if keys != nil && values != nil {
keyExprs = pc.parseExprs4Cond(keys.(ast.Many)...)
valueExprs = pc.parseExprs4Cond(values.(ast.Many)...)
}

entryExprs := pc.compileDictEntryExprs(c, keyExprs, valueExprs)
if entryExprs != nil {
// Generates type DictExpr always to make sure it is easy to do Eval, only process type DictExpr.
result = rel.NewDictExpr(c.(ast.One).Node.Scanner(), false, true, entryExprs...)
Expand All @@ -265,14 +275,8 @@ func (pc ParseContext) compileCond(c ast.Children) rel.Expr {

var controlVarExpr, fExpr rel.Expr

if cNode := c.(ast.One).Node; cNode != nil {
// Only get IDENT or control_var as current grammar
if children, has := cNode.(ast.Branch)["IDENT"]; has {
controlVarExpr = pc.compileIdent(children)
}
if children, has := cNode.(ast.Branch)["control_var"]; has {
controlVarExpr = pc.compileExpr(children)
}
if controlVarNode := b.One("expr"); controlVarNode != nil {
controlVarExpr = pc.CompileExpr(b.One("expr").(ast.Branch))
}

if fNode := c.(ast.One).Node.One("f"); fNode != nil {
Expand Down Expand Up @@ -363,22 +367,27 @@ func (pc ParseContext) compileSet(c ast.Children) rel.Expr {
}

func (pc ParseContext) compileDict(c ast.Children) rel.Expr {
entryExprs := pc.compileDictEntryExprs(c)
entryExprs := pc.compileDictEntryExprs(c, nil, nil)
if entryExprs != nil {
return rel.NewDictExpr(c.(ast.One).Node.Scanner(), false, false, entryExprs...)
}

return rel.NewDict(false)
}

func (pc ParseContext) compileDictEntryExprs(c ast.Children) []rel.DictEntryTupleExpr {
func (pc ParseContext) compileDictEntryExprs(c ast.Children, keyExprs []rel.Expr,
valueExprs []rel.Expr) []rel.DictEntryTupleExpr {
// C* "{" C* dict=((key=@ ":" value=@):",",?) "}" C*
keys := c.(ast.One).Node.(ast.Branch)["key"]
values := c.(ast.One).Node.(ast.Branch)["value"]
if (keys != nil) || (values != nil) {
if (keys != nil) && (values != nil) {
keyExprs := pc.parseExprs(keys.(ast.Many)...)
valueExprs := pc.parseExprs(values.(ast.Many)...)
if keyExprs == nil {
keyExprs = pc.parseExprs(keys.(ast.Many)...)
}
if valueExprs == nil {
valueExprs = pc.parseExprs(values.(ast.Many)...)
}
if len(keyExprs) == len(valueExprs) {
entryExprs := make([]rel.DictEntryTupleExpr, 0, len(keyExprs))
for i, keyExpr := range keyExprs {
Expand Down
36 changes: 36 additions & 0 deletions syntax/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,42 @@ func (pc ParseContext) parseExprs(exprs ...ast.Node) []rel.Expr {
return result
}

// parseExprs4Cond parses conditons/keys and values expressions for syntax `cond`.
func (pc ParseContext) parseExprs4Cond(exprs ...ast.Node) []rel.Expr {
result := make([]rel.Expr, 0, len(exprs))
for _, expr := range exprs {
var exprResult rel.Expr

name, c := which(expr.(ast.Branch), "expr")
if c == nil {
panic(fmt.Errorf("misshapen node AST: %v", expr.(ast.Branch)))
}

if name == "expr" {
switch c := c.(type) {
case ast.One:
exprResult = pc.CompileExpr(c.Node.(ast.Branch))
case ast.Many:
if len(c) == 1 {
exprResult = pc.CompileExpr(c[0].(ast.Branch))
} else {
var elements []rel.Expr
for _, e := range c {
expr := pc.CompileExpr(e.(ast.Branch))
elements = append(elements, expr)
}
exprResult = rel.NewArrayExpr(c.Scanner(), elements...)
}
}
}

if exprResult != nil {
result = append(result, exprResult)
}
}
return result
}

func parseNames(names ast.Branch) []string {
idents := names["IDENT"].(ast.Many)
result := make([]string, 0, len(idents))
Expand Down
Loading

0 comments on commit 2f40b95

Please sign in to comment.