1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
//! The `math()` expression language: an own recursive-descent engine with the semantics of
//! exp4j 0.4.8 as TinkerPop 3.8.2's `MathStep` configures it, plus Graphersal's nested-access
//! extension. Pure: no graph, no Rhai. Parse once with [`MathExpr::parse`], evaluate per
//! traverser with [`MathExpr::eval`] (no allocation per evaluation).
//!
//! Extend the language only through the tables in [`registry`] (`FUNCTIONS`, `BINARY_OPS`,
//! `UNARY_OPS`, `CONSTANTS`) plus one functional test row; the parser and evaluator are generic.
//!
//! # Reference semantics (verified against the sources)
//!
//! Sources: exp4j 0.4.8 (`net.objecthunter.exp4j`: `tokenizer/Tokenizer.java`,
//! `shuntingyard/ShuntingYard.java`, `operator/Operator.java`, `operator/Operators.java`,
//! `function/Functions.java`, `ExpressionBuilder.java`, `Expression.java`) and TinkerPop 3.8.2
//! `gremlin-core/.../traversal/step/map/MathStep.java`.
//!
//! - **Builder options.** `MathStep.TinkerExpression` builds with
//! `ExpressionBuilder(equation).variables(vars).implicitMultiplication(false)`, so `2 x`,
//! `2(3)`, `2 pi`, `1 2` are errors. Variables are collected by `MathStep.VARIABLE_PATTERN`:
//! every `\b[a-zA-Z_][a-zA-Z0-9_]*\b` that does not start with a digit and is not one of the 19
//! function names in `MathStep.FUNCTIONS`, in first-appearance order (`LinkedHashSet`).
//! - **Precedence** (`Operator.PRECEDENCE_*`, higher binds tighter): binary `+ -` 500 <
//! `* / %` 1000 < unary `- +` 5000 < `^` 10000. Binary operators are left-associative except
//! `^` (right). Unary operators are right-associative and never pop a pending binary
//! operator (`ShuntingYard`), so `-2^2 = -4`, `2^-2^2 = 2^(-(2^2))`, `2^3^2 = 512`,
//! `-7%3 = (-7)%3 = -1`, `--2 = 2`, `+-2 = -2`, `2--2 = 4`.
//! - **Unary vs binary** (`Tokenizer.getOperator`): `+`/`-` is unary at the start, after an
//! opening bracket, after `,` and after another operator; everywhere else binary.
//! - **Brackets**: exp4j treats `(`, `[`, `{` all as parentheses. Graphersal accepts all three
//! for grouping and calls, but requires the closer to match the opener (exp4j accepts `(1]`).
//! - **Functions** (`Functions.getBuiltinFunction`), 23, all arity 1 except `pow` (2):
//! `abs acos asin atan cbrt ceil cos cosh cot exp expm1 floor log log10 log1p log2 pow signum
//! sin sinh sqrt tan tanh`. `log` is the natural log; `log2(x)` is `log(x)/log(2)`;
//! `signum` returns 0 for NaN and -0.0; `cot(x)` throws "Division by zero in cotangent!" where
//! `tan(x) == 0`. Domain errors are not errors: `sqrt(-1)` and `log(-1)` are NaN,
//! `log(0)` is -inf (`java.lang.Math`). `pow`/`^` follow `Math.pow` (NaN exponent → NaN,
//! `(±1)^±inf` → NaN), see [`registry::java_pow`].
//! - **Function call syntax.** A function token is pushed on the operator stack and is only
//! emitted by a closing parenthesis or at the end, so a function name *without* parentheses
//! applies to everything to its right up to the end of the enclosing group:
//! `sin _` = `sin(_)`, `sin _ + 1` = `sin(_ + 1)`, `2 * sin _ + 1` = `2 * sin(_ + 1)`,
//! `(sin _ + 1) * 2` = `sin(_ + 1) * 2`. A `+`/`-` right after a function name is a binary
//! operator in exp4j, so `sin -1` fails; Graphersal rejects it with a hint to write `sin(-1)`.
//! exp4j does not check the argument count syntactically (it pops `numArguments` values at
//! evaluation and fails on a stack mismatch); Graphersal checks the arity at parse time.
//! - **Constants** (`Expression.createDefaultVariables`): `pi` = `π` = `Math.PI`, `e` =
//! `Math.E`, `φ` = 1.61803398874 (exp4j's truncated literal). exp4j implements them as
//! pre-set variables, so a declared variable of the same name overrides the value. `pi`/`e`
//! match `MathStep.VARIABLE_PATTERN`, so TinkerPop declares them as step variables and
//! resolves them through `Scoping` (a scope key `e` wins; without one TinkerPop throws
//! "must resolve to a Number"). `π`/`φ` never match the ASCII pattern and are plain constants.
//! - **Division by zero**: `/` and `%` throw `ArithmeticException("Division by zero!")` when the
//! divisor `== 0d` (also `-0.0`); a NaN divisor yields NaN; `x/inf` is 0.
//! - **Number literals** (`Tokenizer.parseNumberToken` + `Double.parseDouble`): start with a
//! digit or `.`, greedily continue over digits, `.`, `e`, `E`, and `+`/`-` right after
//! `e`/`E`; a trailing `e`/`E` is given back. `12`, `1.5`, `.5`, `5.`, `1e3`, `1E-3`,
//! `2.5e+2`, `2.e3` are valid; `1.2.3`, `1e+`, `1ee3` are errors. No sign, hex or `inf`.
//! - **Identifiers** (`Tokenizer.parseFunctionOrVariable`): exp4j takes the longest prefix that
//! is a known variable or function. With MathStep's declared variables that equals maximal
//! munch over `[A-Za-z_][A-Za-z0-9_]*`: `sinx`, `sinh2`, `log2x` are variables, `sin(x)` a
//! call. Whitespace is skipped between tokens (`Character.isWhitespace`); an empty or blank
//! expression is an error ("Expression can not be empty").
//!
//! # Graphersal deviations
//!
//! - `cot`, `expm1`, `log1p`, `pow` are usable. In TinkerPop they are missing from
//! `MathStep.FUNCTIONS`, get declared as variables, and `ExpressionBuilder.build()` throws "A
//! variable can not have the same name as a function" — so no working TinkerPop query uses them.
//! - `pi`/`e` are variables (they take part in [`MathExpr::variables`] and the `by()` ring as in
//! TinkerPop) that carry their constant as a fallback ([`VarRef::constant`]): a resolver uses
//! a scope value when one exists and the constant otherwise, where TinkerPop would throw.
//! - Arity and a signed paren-less argument are parse errors (exp4j: evaluation errors).
//! - Brackets must match in kind.
//! - **Nested access** extension, after any variable, with no whitespace before `.`/`[`:
//! `_.a.b`, `_[0]`, `_.items[2].price`, `_["unit price"]`, `_['k']`, `a.b + c.d[1]`.
//! Keys after `.` follow the identifier grammar; quoted keys use `"` or `'` with the escapes
//! `\\`, `\"`, `\'`; an index is an optionally negative decimal integer (its meaning is the
//! resolver's). exp4j rejects every such expression (a `.` after a name starts an invalid
//! number; `[` after a name needs implicit multiplication), so no TinkerPop expression changes
//! meaning.
//! - Nesting deeper than [`MAX_DEPTH`] (brackets, operators, calls) is a parse error instead of
//! a stack overflow.
pub
use CompactString;
use ;
pub use Arity;
/// Maximum nesting depth of an expression (brackets, operator operands, call arguments).
pub const MAX_DEPTH: usize = 256;
/// Maximum number of arguments of one function call (the evaluator uses a fixed stack buffer).
pub const MAX_CALL_ARGS: usize = 8;
/// One step of a nested-access path (`_.a`, `_[0]`, `_["unit price"]`).
pub
/// A variable occurrence: which variable, the nested-access path applied to its value, and
/// where it is in the source.
pub
/// One instruction of the compiled postfix program.
pub
/// A parsed `math()` expression, compiled to a postfix program. Parse once, evaluate many times.
pub
/// A parse error before the column is computed (internal).
pub
/// Why a `math()` expression could not be parsed, and where.
pub
/// The kinds of [`MathParseError`].
pub
/// A failure while evaluating a parsed expression, and where.
pub
/// The kinds of [`MathEvalError`]; the messages are exp4j's.
pub