Skip to content

Commit 9ca99f0

Browse files
authored
Add pipe operator (#400)
1 parent 9da6e0f commit 9ca99f0

File tree

8 files changed

+161
-55
lines changed

8 files changed

+161
-55
lines changed

debug/go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ module github.com/antonmedv/expr/debug
33
go 1.13
44

55
require (
6-
github.com/antonmedv/expr v1.9.1-0.20221030193158-2213166cdca2
6+
github.com/antonmedv/expr v0.0.0
77
github.com/gdamore/tcell v1.3.0
88
github.com/rivo/tview v0.0.0-20200219210816-cd38d7432498
99
)
10+
11+
replace github.com/antonmedv/expr => ../

debug/go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
2-
github.com/antonmedv/expr v1.9.1-0.20221030193158-2213166cdca2 h1:/lRT2yHd7/W9hW2ZvpNKyoop5YBjJ+wa6p6Vxg1m9Hg=
3-
github.com/antonmedv/expr v1.9.1-0.20221030193158-2213166cdca2/go.mod h1:FPC8iWArxls7axbVLsW+kpg1mz29A1b2M6jt+hZfDkU=
42
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
53
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
64
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

parser/lexer/state.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,17 @@ func root(l *lexer) stateFn {
2828
return questionMark
2929
case r == '/':
3030
return slash
31+
case r == '|':
32+
l.accept("|")
33+
l.emit(Operator)
3134
case strings.ContainsRune("([{", r):
3235
l.emit(Bracket)
3336
case strings.ContainsRune(")]}", r):
3437
l.emit(Bracket)
3538
case strings.ContainsRune("#,:%+-^", r): // single rune operator
3639
l.emit(Operator)
37-
case strings.ContainsRune("&|!=*<>", r): // possible double rune operator
38-
l.accept("&|=*")
40+
case strings.ContainsRune("&!=*<>", r): // possible double rune operator
41+
l.accept("&=*")
3942
l.emit(Operator)
4043
case r == '.':
4144
l.backup()

parser/parser.go

Lines changed: 89 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,6 @@ type operator struct {
2323
associativity associativity
2424
}
2525

26-
type builtin struct {
27-
arity int
28-
}
29-
3026
var unaryOperators = map[string]operator{
3127
"not": {50, left},
3228
"!": {50, left},
@@ -61,6 +57,10 @@ var binaryOperators = map[string]operator{
6157
"??": {500, left},
6258
}
6359

60+
type builtin struct {
61+
arity int
62+
}
63+
6464
var builtins = map[string]builtin{
6565
"all": {2},
6666
"none": {2},
@@ -201,12 +201,41 @@ func (p *parser) parseExpression(precedence int) Node {
201201
}
202202

203203
if precedence == 0 {
204-
nodeLeft = p.parseConditionalExpression(nodeLeft)
204+
nodeLeft = p.parseConditional(nodeLeft)
205+
206+
if p.current.Is(Operator, "|") {
207+
p.next()
208+
return p.parsePipe(nodeLeft)
209+
}
205210
}
206211

207212
return nodeLeft
208213
}
209214

215+
func (p *parser) parseConditional(node Node) Node {
216+
var expr1, expr2 Node
217+
for p.current.Is(Operator, "?") && p.err == nil {
218+
p.next()
219+
220+
if !p.current.Is(Operator, ":") {
221+
expr1 = p.parseExpression(0)
222+
p.expect(Operator, ":")
223+
expr2 = p.parseExpression(0)
224+
} else {
225+
p.next()
226+
expr1 = node
227+
expr2 = p.parseExpression(0)
228+
}
229+
230+
node = &ConditionalNode{
231+
Cond: node,
232+
Exp1: expr1,
233+
Exp2: expr2,
234+
}
235+
}
236+
return node
237+
}
238+
210239
func (p *parser) parsePrimary() Node {
211240
token := p.current
212241

@@ -245,34 +274,10 @@ func (p *parser) parsePrimary() Node {
245274
}
246275
}
247276

248-
return p.parsePrimaryExpression()
249-
}
250-
251-
func (p *parser) parseConditionalExpression(node Node) Node {
252-
var expr1, expr2 Node
253-
for p.current.Is(Operator, "?") && p.err == nil {
254-
p.next()
255-
256-
if !p.current.Is(Operator, ":") {
257-
expr1 = p.parseExpression(0)
258-
p.expect(Operator, ":")
259-
expr2 = p.parseExpression(0)
260-
} else {
261-
p.next()
262-
expr1 = node
263-
expr2 = p.parseExpression(0)
264-
}
265-
266-
node = &ConditionalNode{
267-
Cond: node,
268-
Exp1: expr1,
269-
Exp2: expr2,
270-
}
271-
}
272-
return node
277+
return p.parseSecondary()
273278
}
274279

275-
func (p *parser) parsePrimaryExpression() Node {
280+
func (p *parser) parseSecondary() Node {
276281
var node Node
277282
token := p.current
278283

@@ -294,7 +299,7 @@ func (p *parser) parsePrimaryExpression() Node {
294299
node.SetLocation(token.Location)
295300
return node
296301
default:
297-
node = p.parseIdentifierExpression(token)
302+
node = p.parseCall(token)
298303
}
299304

300305
case Number:
@@ -345,14 +350,13 @@ func (p *parser) parsePrimaryExpression() Node {
345350
return p.parsePostfixExpression(node)
346351
}
347352

348-
func (p *parser) parseIdentifierExpression(token Token) Node {
353+
func (p *parser) parseCall(token Token) Node {
349354
var node Node
350355
if p.current.Is(Bracket, "(") {
351356
var arguments []Node
352357

353358
if b, ok := builtins[token.Value]; ok {
354359
p.expect(Bracket, "(")
355-
// TODO: Add builtins signatures.
356360
if b.arity == 1 {
357361
arguments = make([]Node, 1)
358362
arguments[0] = p.parseExpression(0)
@@ -578,20 +582,43 @@ func (p *parser) parsePostfixExpression(node Node) Node {
578582
return node
579583
}
580584

581-
func isValidIdentifier(str string) bool {
582-
if len(str) == 0 {
583-
return false
584-
}
585-
h, w := utf8.DecodeRuneInString(str)
586-
if !IsAlphabetic(h) {
587-
return false
588-
}
589-
for _, r := range str[w:] {
590-
if !IsAlphaNumeric(r) {
591-
return false
585+
func (p *parser) parsePipe(node Node) Node {
586+
identifier := p.current
587+
p.expect(Identifier)
588+
589+
arguments := []Node{node}
590+
591+
if b, ok := builtins[identifier.Value]; ok {
592+
p.expect(Bracket, "(")
593+
if b.arity == 2 {
594+
arguments = append(arguments, p.parseClosure())
595+
}
596+
p.expect(Bracket, ")")
597+
598+
node = &BuiltinNode{
599+
Name: identifier.Value,
600+
Arguments: arguments,
592601
}
602+
node.SetLocation(identifier.Location)
603+
} else {
604+
callee := &IdentifierNode{Value: identifier.Value}
605+
callee.SetLocation(identifier.Location)
606+
607+
arguments = append(arguments, p.parseArguments()...)
608+
609+
node = &CallNode{
610+
Callee: callee,
611+
Arguments: arguments,
612+
}
613+
node.SetLocation(identifier.Location)
593614
}
594-
return true
615+
616+
if p.current.Is(Operator, "|") {
617+
p.next()
618+
return p.parsePipe(node)
619+
}
620+
621+
return node
595622
}
596623

597624
func (p *parser) parseArguments() []Node {
@@ -608,3 +635,19 @@ func (p *parser) parseArguments() []Node {
608635

609636
return nodes
610637
}
638+
639+
func isValidIdentifier(str string) bool {
640+
if len(str) == 0 {
641+
return false
642+
}
643+
h, w := utf8.DecodeRuneInString(str)
644+
if !IsAlphabetic(h) {
645+
return false
646+
}
647+
for _, r := range str[w:] {
648+
if !IsAlphaNumeric(r) {
649+
return false
650+
}
651+
}
652+
return true
653+
}

parser/parser_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
. "github.com/antonmedv/expr/ast"
99
"github.com/antonmedv/expr/parser"
1010
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
1112
)
1213

1314
func TestParse(t *testing.T) {
@@ -444,6 +445,12 @@ func TestParse(t *testing.T) {
444445
Left: &IdentifierNode{Value: "foo"},
445446
Right: &CallNode{Callee: &IdentifierNode{Value: "bar"}}},
446447
},
448+
{
449+
"true | ok()",
450+
&CallNode{
451+
Callee: &IdentifierNode{Value: "ok"},
452+
Arguments: []Node{
453+
&BoolNode{Value: true}}}},
447454
}
448455
for _, test := range parseTests {
449456
actual, err := parser.Parse(test.input)
@@ -631,3 +638,26 @@ func TestParse_optional_chaining(t *testing.T) {
631638
assert.Equal(t, Dump(test.expected), Dump(actual.Node), test.input)
632639
}
633640
}
641+
642+
func TestParse_pipe_operator(t *testing.T) {
643+
input := "arr | map(.foo) | len() | Foo()"
644+
expect := &CallNode{
645+
Callee: &IdentifierNode{Value: "Foo"},
646+
Arguments: []Node{
647+
&CallNode{
648+
Callee: &IdentifierNode{Value: "len"},
649+
Arguments: []Node{
650+
&BuiltinNode{
651+
Name: "map",
652+
Arguments: []Node{
653+
&IdentifierNode{Value: "arr"},
654+
&ClosureNode{
655+
Node: &MemberNode{
656+
Node: &PointerNode{},
657+
Property: &StringNode{Value: "foo"},
658+
}}}}}}}}
659+
660+
actual, err := parser.Parse(input)
661+
require.NoError(t, err)
662+
assert.Equal(t, Dump(expect), Dump(actual.Node))
663+
}

repl/go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ module github.com/antonmedv/expr/repl
33
go 1.20
44

55
require (
6-
github.com/antonmedv/expr v1.12.7
6+
github.com/antonmedv/expr v0.0.0
77
github.com/chzyer/readline v1.5.1
88
)
99

1010
require golang.org/x/sys v0.11.0 // indirect
11+
12+
replace github.com/antonmedv/expr => ../

repl/go.sum

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
github.com/antonmedv/expr v1.12.7 h1:jfV/l/+dHWAadLwAtESXNxXdfbK9bE4+FNMHYCMntwk=
2-
github.com/antonmedv/expr v1.12.7/go.mod h1:FPC8iWArxls7axbVLsW+kpg1mz29A1b2M6jt+hZfDkU=
31
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
42
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
53
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
@@ -16,7 +14,6 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
1614
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
1715
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
1816
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
19-
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 h1:y/woIyUBFbpQGKS0u1aHF/40WUDnek3fPOyD08H5Vng=
2017
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
2118
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
2219
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

test/pipes/pipes_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package pipes_test
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/antonmedv/expr"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestPipes(t *testing.T) {
12+
env := map[string]interface{}{
13+
"sprintf": fmt.Sprintf,
14+
}
15+
16+
program, err := expr.Compile(`"%s bar %d" | sprintf("foo", -42 | abs())`, expr.Env(env))
17+
require.NoError(t, err)
18+
19+
out, err := expr.Run(program, env)
20+
require.NoError(t, err)
21+
require.Equal(t, "foo bar 42", out)
22+
}
23+
24+
func TestPipes_map_filter(t *testing.T) {
25+
program, err := expr.Compile(`1..9 | map(# + 1) | filter(# % 2 == 0)`)
26+
require.NoError(t, err)
27+
28+
out, err := expr.Run(program, nil)
29+
require.NoError(t, err)
30+
require.Equal(t, []interface{}{2, 4, 6, 8, 10}, out)
31+
}

0 commit comments

Comments
 (0)