Skip to content

Commit e208fcb

Browse files
committed
v0.6.0
1 parent 2b4af42 commit e208fcb

4 files changed

Lines changed: 155 additions & 82 deletions

File tree

README.md

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
## What is it?
44

5-
This project is a renewed version of expression parser to use in jsonslice.
5+
This project is a renewed version of expression parser used in [jsonslice](https://github.com/bhmj/jsonslice). It is still work in progress so use it with caution.
66

7-
## Usage
7+
## Check it out
88

99
`go run ./... "expression"`
1010

@@ -16,22 +16,71 @@ Expression examples:
1616
`5 + -5`
1717
`1/(3 & 5)`
1818
`'a' > 'b'`
19-
`'abc' ~= /a.c/i`
19+
`'abc' =~ /a.c/i`
2020
`!((false))`
2121

22+
## Operators and data types supported
23+
24+
Operators |  
25+
--- | ---
26+
Arithmetic | `+` `-` `*` `/` `**` `%`
27+
Bitwise | `\|` `&` `^` `~` `<<` `>>`
28+
Logical | `&&` `\|\|` `!`
29+
Comparison | `==` `!=` `===` `!==` `>=` `>` `<=` `<`
30+
Regexp | `=~` `!=~` `!~`
31+
Parentheses | `(` `)`
32+
33+
<b>Data types</b> | &nbsp;
34+
--- | ---
35+
String constants | `'string'` or `"string"`
36+
Numeric | 64-bit integers or floats
37+
Boolean | `true` or `false`. Comparison results in boolean value.
38+
Regexp | `/expression/` with modifiers: `i` (case-insensitive), `m` (multiline), `s` (single-line), `U` (ungreedy).<br/>Example: `/ab.*d/iU`
39+
Other | `null`
40+
2241
## Test coverage
2342

2443
Tests cover the majority of cases described in ECMAScript Language definition (specifically [ECMAScript Language: Expressions reference](https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html) and [Testing and Comparison Operations](https://tc39.es/ecma262/multipage/abstract-operations.html#sec-testing-and-comparison-operations)).
2544

45+
## Benchmarks
46+
47+
Evaluate `(2) + (2) == (4)`
48+
49+
```diff
50+
$ go test -bench=. -benchmem -benchtime=4s
51+
goos: windows
52+
goarch: amd64
53+
pkg: github.com/bhmj/expression_parser
54+
Benchmark_ModifiedNumericLiteral_WithParsing-4 1252026 3753 ns/op 1144 B/op 24 allocs/op
55+
Benchmark_ModifiedNumericLiteral_WithoutParsing-4 32295195 158 ns/op 0 B/op 0 allocs/op
56+
PASS
57+
ok github.com/bhmj/expression_parser 16.605
58+
```
59+
60+
The same expression evaluated with [github.com/Knetic/govaluate](https://github.com/Knetic/govaluate) :
61+
62+
```diff
63+
$ go test -bench='LiteralModifiers' -benchmem -benchtime=4s
64+
goos: windows
65+
goarch: amd64
66+
pkg: github.com/Knetic/govaluate
67+
BenchmarkEvaluationLiteralModifiers_WithParsing-4 559497 8499 ns/op 2272 B/op 49 allocs/op
68+
BenchmarkEvaluationLiteralModifiers-4 10908465 403 ns/op 8 B/op 1 allocs/op
69+
PASS
70+
ok github.com/Knetic/govaluate 12.603s
71+
```
72+
73+
2674
## Changelog
2775

76+
**0.6.0** (2021-11-05) -- a remainder operator `%` added. Benchmarks added. Some optimization done.
2877
**0.5.0** (2021-11-04) -- Tests added. Multiple bugs fixed.
2978
**0.4.0** (2021-11-02) -- Expression evaluation.
3079
**0.3.0** (2021-11-01) -- MVP.
3180

3281
## Roadmap
3382

34-
- [x] arithmetic operators: `+ - * / **`
83+
- [x] arithmetic operators: `+ - * / **` `%`
3584
- [x] bitwise operators: `| & ^ ~`
3685
- [x] logical operators: `|| && !`
3786
- [x] comparison operators: `> < >= <= == === != !==`
@@ -43,6 +92,7 @@ Tests cover the majority of cases described in ECMAScript Language definition (s
4392
- [x] expression evaluation
4493
- [x] evaluator test coverage
4594
- [ ] add external reference type (node reference in jsonslice)
95+
- [ ] Unicode support!
4696

4797
## Contributing
4898

evaluator.go

Lines changed: 64 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ var (
1515
byte(opPlus),
1616
byte(opMinus),
1717
byte(opMultiply),
18-
byte(opMultiply),
1918
byte(opDivide),
19+
byte(opRemainder),
2020
byte(opExponentiation),
2121
byte(opBitwiseOR),
2222
byte(opBitwiseXOR),
@@ -83,8 +83,6 @@ func evaluateExpression(tokens []*Token) (*Operand, []*Token, error) {
8383
// execOperator takes an Operator and one or two Operands (the second one can be `nil` depending on operator type - unary or binary).
8484
// It does evaluate the expression ("operand1 operator operand2" or "operator operand1") and return Operand which is a typed value.
8585
func execOperator(op Operator, left *Operand, right *Operand) (*Operand, error) {
86-
var res Operand
87-
8886
if bytes.IndexByte(opsArithmetic, byte(op)) != -1 {
8987
// arithmetic
9088
return doArithmetic(op, left, right)
@@ -95,86 +93,94 @@ func execOperator(op Operator, left *Operand, right *Operand) (*Operand, error)
9593
// logic
9694
return doLogic(op, left, right)
9795
}
98-
return &res, errUnknownToken
96+
return nil, errUnknownToken
9997
}
10098

10199
// doArithmetic actually evaluates the arithmetic operators.
102100
// Note the special case of string concatenation: string + any_type -> string
103101
func doArithmetic(op Operator, left *Operand, right *Operand) (*Operand, error) {
104-
var res Operand
105-
106102
if op == opPlus && (left.Type|right.Type)&otString > 0 {
107103
// string concatenation
108-
res.Type = otString
109-
lstr := toString(left)
110-
rstr := toString(right)
111-
res.Str = append(lstr.Str, rstr.Str...)
112-
return &res, nil
104+
toString(left)
105+
toString(right)
106+
left.Str = append(left.Str[:len(left.Str):len(left.Str)], right.Str...) // cannot use left buffer, must reallocate!
107+
return left, nil
113108
}
114109

115-
res.Type = otNumber
116110
switch op {
117111
case opUnaryMinus:
118-
res.Number = -toNumber(left).Number
112+
left.Number = -toNumber(left).Number
119113
case opPlus:
120-
res.Number = toNumber(left).Number + toNumber(right).Number
114+
left.Number = toNumber(left).Number + toNumber(right).Number
121115
case opMinus:
122-
res.Number = toNumber(left).Number - toNumber(right).Number
116+
left.Number = toNumber(left).Number - toNumber(right).Number
123117
case opMultiply:
124-
res.Number = toNumber(left).Number * toNumber(right).Number
118+
left.Number = toNumber(left).Number * toNumber(right).Number
125119
case opDivide:
126-
res.Number = toNumber(left).Number / toNumber(right).Number
120+
left.Number = toNumber(left).Number / toNumber(right).Number
121+
case opRemainder:
122+
left.Number = float64(int64(toNumber(left).Number) % int64(toNumber(right).Number))
127123
case opExponentiation:
128-
res.Number = math.Pow(toNumber(left).Number, toNumber(right).Number)
124+
left.Number = math.Pow(toNumber(left).Number, toNumber(right).Number)
129125
case opBitwiseAND:
130-
res.Number = float64(int64(toNumber(left).Number) & int64(toNumber(right).Number))
126+
left.Number = float64(int64(toNumber(left).Number) & int64(toNumber(right).Number))
131127
case opBitwiseOR:
132-
res.Number = float64(int64(toNumber(left).Number) | int64(toNumber(right).Number))
128+
left.Number = float64(int64(toNumber(left).Number) | int64(toNumber(right).Number))
133129
case opBitwiseXOR:
134-
res.Number = float64(int64(toNumber(left).Number) ^ int64(toNumber(right).Number))
130+
left.Number = float64(int64(toNumber(left).Number) ^ int64(toNumber(right).Number))
135131
case opBitwiseNOT:
136-
res.Number = float64(^int64(toNumber(left).Number))
132+
left.Number = float64(^int64(toNumber(left).Number))
137133
case opShiftLeft:
138-
res.Number = float64(int64(toNumber(left).Number) << int64(toNumber(right).Number))
134+
left.Number = float64(int64(toNumber(left).Number) << int64(toNumber(right).Number))
139135
case opShiftRight:
140-
res.Number = float64(int64(toNumber(left).Number) >> int64(toNumber(right).Number))
136+
left.Number = float64(int64(toNumber(left).Number) >> int64(toNumber(right).Number))
141137
}
142-
return &res, nil
138+
left.Type = otNumber
139+
return left, nil
143140
}
144141

145142
// doComaparison compares two operands. The special case is string regexp match which works only on strings.
146143
// Otherwise works like JS comparison.
147144
func doComparison(op Operator, left *Operand, right *Operand) (*Operand, error) {
148-
result := Operand{Type: otBoolean, Bool: false}
145+
result := left
149146

150147
comparedTypes := left.Type | right.Type
151148

152149
// [1] 7.2.14 (2,3)
153150
if op == opEqual && comparedTypes&(otNull|otUndefined) > 0 {
154151
// at least one side is null or undefined:
152+
result.Type = otBoolean
155153
result.Bool = (comparedTypes | otNull | otUndefined) == (otNull | otUndefined) // both are null or undefined
156-
return &result, nil
154+
return result, nil
157155
}
158156
if op == opRegexMatch || op == opNotRegexMatch {
159157
// one of them must be regexp
160158
if comparedTypes&otRegexp != otRegexp {
161-
return &result, nil
159+
result.Type = otBoolean
160+
result.Bool = false
161+
return result, nil
162162
}
163163
// other must be non-regexp
164164
if comparedTypes-otRegexp == 0 {
165-
return &result, nil
165+
result.Type = otBoolean
166+
result.Bool = false
167+
return result, nil
166168
}
167169
// convert non-regexp part to string and compare
168170
if right.Type == otRegexp {
169-
return doCompareString(op, toString(left), right) // regexp should be second argument
171+
toString(left)
172+
return doCompareString(op, left, right) // regexp should be second argument
170173
}
171-
return doCompareString(op, toString(right), left) // regexp should be second argument
174+
toString(right)
175+
return doCompareString(op, right, left) // regexp should be second argument
172176
}
173177

174178
// [1] 7.2.15 (1)
175179
if (op == opStrictEqual || op == opStrictNotEqual) && left.Type != right.Type {
176180
// strict comparison: types must match
177-
return &result, nil
181+
result.Type = otBoolean
182+
result.Bool = false
183+
return result, nil
178184
}
179185

180186
// [1] 7.2.15 (4)
@@ -188,26 +194,23 @@ func doComparison(op Operator, left *Operand, right *Operand) (*Operand, error)
188194

189195
// doLogic executes binary logical operators following JavaScript conversion rules.
190196
func doLogic(op Operator, left *Operand, right *Operand) (*Operand, error) {
191-
var res Operand
192-
res.Type = otBoolean
193-
194197
lval := toBoolean(left)
195198
if op == opLogicalAND || op == opLogicalOR {
196-
if (op == opLogicalAND && !lval.Bool) || (op == opLogicalOR && lval.Bool) { // false AND ..., true OR ... -> result!
199+
if (op == opLogicalAND && !lval) || (op == opLogicalOR && lval) { // false AND ..., true OR ... -> result!
197200
return left, nil
198201
}
199202
return right, nil
200203
}
201204

202205
// logical NOT
203-
lval.Bool = !lval.Bool
204-
return lval, nil
206+
left.Type = otBoolean
207+
left.Bool = !lval
208+
return left, nil
205209
}
206210

207211
// doCompareNumber compares two numbers.
208212
func doCompareNumber(op Operator, left *Operand, right *Operand) (*Operand, error) {
209-
var res Operand
210-
res.Type = otBoolean
213+
res := left
211214
switch op {
212215
case opG:
213216
res.Bool = left.Number > right.Number
@@ -222,13 +225,13 @@ func doCompareNumber(op Operator, left *Operand, right *Operand) (*Operand, erro
222225
case opLE:
223226
res.Bool = left.Number <= right.Number
224227
}
225-
return &res, nil
228+
res.Type = otBoolean
229+
return res, nil
226230
}
227231

228232
// doCompareString compares two strings.
229233
func doCompareString(op Operator, left *Operand, right *Operand) (*Operand, error) {
230-
var res Operand
231-
res.Type = otBoolean
234+
res := left
232235
switch op {
233236
case opEqual, opStrictEqual:
234237
res.Bool = compareSlices(left.Str, right.Str) == 0
@@ -247,9 +250,10 @@ func doCompareString(op Operator, left *Operand, right *Operand) (*Operand, erro
247250
case opNotRegexMatch:
248251
res.Bool = !right.Regexp.MatchString(string(left.Str))
249252
default:
250-
return left, errUnknownToken
253+
return res, errUnknownToken
251254
}
252-
return &res, nil
255+
res.Type = otBoolean
256+
return res, nil
253257
}
254258

255259
// toString converts operand to string following JavaScript conversion rules.
@@ -258,8 +262,7 @@ func toString(op *Operand) *Operand {
258262
return op
259263
}
260264

261-
result := *op
262-
result.Type = otString
265+
result := op
263266

264267
switch op.Type {
265268
case otUndefined:
@@ -273,9 +276,11 @@ func toString(op *Operand) *Operand {
273276
result.Str = []byte("false")
274277
}
275278
case otNumber:
276-
result.Str = []byte(strconv.FormatFloat(result.Number, 'f', -1, 64))
279+
result.Str = []byte(strconv.FormatFloat(op.Number, 'f', -1, 64))
277280
}
278-
return &result
281+
result.Type = otString
282+
283+
return result
279284
}
280285

281286
// toNumber converts operand to number following JavaScript conversion rules. See [1] 7.1.4
@@ -284,8 +289,7 @@ func toNumber(op *Operand) *Operand {
284289
return op
285290
}
286291

287-
result := *op
288-
result.Type = otNumber
292+
result := op
289293

290294
switch op.Type {
291295
case otUndefined:
@@ -311,28 +315,25 @@ func toNumber(op *Operand) *Operand {
311315
case otRegexp:
312316
result.Number = math.NaN()
313317
}
314-
return &result
318+
result.Type = otNumber
319+
return result
315320
}
316321

317322
// toBoolean converts operand to boolean following JavaScript conversion rules.
318-
func toBoolean(op *Operand) *Operand {
323+
func toBoolean(op *Operand) bool {
319324
if op.Type == otBoolean {
320-
return op
325+
return op.Bool
321326
}
322327

323-
result := *op
324-
result.Type = otBoolean
328+
result := false
325329
switch op.Type {
326-
case otUndefined, otNull:
327-
result.Bool = false
330+
case otUndefined, otNull, otRegexp:
328331
case otString:
329-
result.Bool = len(op.Str) > 0
332+
result = len(op.Str) > 0
330333
case otNumber:
331-
result.Bool = op.Number != 0 && !math.IsNaN(op.Number)
332-
case otRegexp:
333-
result.Bool = false
334+
result = op.Number != 0 && !math.IsNaN(op.Number)
334335
}
335-
return &result
336+
return result
336337
}
337338

338339
// logic complies to [1] 7.2.13 (3) only for BYTES not codepoints!

0 commit comments

Comments
 (0)