line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Language::FormulaEngine; |
2
|
6
|
|
|
6
|
|
1412554
|
use Moo; |
|
6
|
|
|
|
|
69313
|
|
|
6
|
|
|
|
|
32
|
|
3
|
6
|
|
|
6
|
|
9560
|
use Carp; |
|
6
|
|
|
|
|
14
|
|
|
6
|
|
|
|
|
324
|
|
4
|
6
|
|
|
6
|
|
1700
|
use Try::Tiny; |
|
6
|
|
|
|
|
4141
|
|
|
6
|
|
|
|
|
326
|
|
5
|
6
|
|
|
6
|
|
42
|
use Module::Runtime 'require_module'; |
|
6
|
|
|
|
|
12
|
|
|
6
|
|
|
|
|
46
|
|
6
|
|
|
|
|
|
|
|
7
|
|
|
|
|
|
|
# ABSTRACT: Parser/Interpreter/Compiler for simple spreadsheet formula language |
8
|
|
|
|
|
|
|
our $VERSION = '0.06'; # VERSION |
9
|
|
|
|
|
|
|
|
10
|
|
|
|
|
|
|
|
11
|
|
|
|
|
|
|
has parser => ( |
12
|
|
|
|
|
|
|
is => 'lazy', |
13
|
|
|
|
6
|
|
|
builder => sub {}, |
14
|
|
|
|
|
|
|
coerce => sub { _coerce_instance($_[0], 'parse', 'Language::FormulaEngine::Parser') } |
15
|
|
|
|
|
|
|
); |
16
|
|
|
|
|
|
|
has namespace => ( |
17
|
|
|
|
|
|
|
is => 'lazy', |
18
|
|
|
|
3
|
|
|
builder => sub {}, |
19
|
|
|
|
|
|
|
coerce => sub { _coerce_instance($_[0], 'get_function', 'Language::FormulaEngine::Namespace::Default') }, |
20
|
|
|
|
|
|
|
trigger => sub { my ($self, $val)= @_; $self->compiler->namespace($val) }, |
21
|
|
|
|
|
|
|
); |
22
|
|
|
|
|
|
|
has compiler => ( |
23
|
|
|
|
|
|
|
is => 'lazy', |
24
|
|
|
|
5
|
|
|
builder => sub {}, |
25
|
|
|
|
|
|
|
coerce => sub { _coerce_instance($_[0], 'compile', 'Language::FormulaEngine::Compiler') } |
26
|
|
|
|
|
|
|
); |
27
|
|
|
|
|
|
|
|
28
|
|
|
|
|
|
|
sub BUILD { |
29
|
6
|
|
|
6
|
0
|
10894
|
my $self= shift; |
30
|
6
|
|
|
|
|
113
|
$self->compiler->namespace($self->namespace); |
31
|
|
|
|
|
|
|
} |
32
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
sub _coerce_instance { |
34
|
18
|
|
|
18
|
|
74
|
my ($thing, $req_method, $default_class)= @_; |
35
|
18
|
100
|
100
|
|
|
133
|
return $thing if ref $thing and ref($thing)->can($req_method); |
36
|
|
|
|
|
|
|
|
37
|
|
|
|
|
|
|
my $class= !defined $thing? $default_class |
38
|
17
|
0
|
66
|
|
|
71
|
: ref $thing eq 'HASH'? $thing->{CLASS} || $default_class |
|
|
0
|
0
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
39
|
|
|
|
|
|
|
: ref $thing? $default_class |
40
|
|
|
|
|
|
|
: ($req_method eq 'get_function' && $thing =~ /^[0-9]+$/)? "Language::FormulaEngine::Namespace::Default::V$thing" |
41
|
|
|
|
|
|
|
: $thing; |
42
|
17
|
100
|
|
|
|
214
|
require_module($class) |
43
|
|
|
|
|
|
|
unless $class->can($req_method); |
44
|
|
|
|
|
|
|
|
45
|
17
|
50
|
|
|
|
90
|
my @args= !ref $thing? () |
|
|
100
|
|
|
|
|
|
46
|
|
|
|
|
|
|
: (ref $thing eq 'ARRAY')? @$thing |
47
|
|
|
|
|
|
|
: $thing; |
48
|
17
|
|
|
|
|
188
|
return $class->new(@args); |
49
|
|
|
|
|
|
|
} |
50
|
|
|
|
|
|
|
|
51
|
|
|
|
|
|
|
|
52
|
|
|
|
|
|
|
sub evaluate { |
53
|
126
|
|
|
126
|
1
|
793433
|
my ($self, $text, $vars)= @_; |
54
|
126
|
50
|
|
|
|
3380
|
$self->parser->parse($text) |
55
|
|
|
|
|
|
|
or die $self->parser->error; |
56
|
126
|
|
|
|
|
2781
|
my $ns= $self->namespace; |
57
|
126
|
50
|
33
|
|
|
1683
|
$ns= $ns->clone_and_merge(variables => $vars) if $vars && %$vars; |
58
|
126
|
|
|
|
|
5116
|
return $self->parser->parse_tree->evaluate($ns); |
59
|
|
|
|
|
|
|
} |
60
|
|
|
|
|
|
|
|
61
|
|
|
|
|
|
|
|
62
|
|
|
|
|
|
|
sub compile { |
63
|
129
|
|
|
129
|
1
|
19573
|
my ($self, $text)= @_; |
64
|
129
|
50
|
|
|
|
2917
|
$self->parser->parse($text) |
65
|
|
|
|
|
|
|
or die $self->parser->error; |
66
|
129
|
|
|
|
|
2733
|
$self->compiler->namespace($self->namespace); |
67
|
129
|
50
|
|
|
|
2154
|
$self->compiler->compile($self->parser->parse_tree) |
68
|
|
|
|
|
|
|
or die $self->compiler->error; |
69
|
|
|
|
|
|
|
} |
70
|
|
|
|
|
|
|
|
71
|
|
|
|
|
|
|
|
72
|
|
|
|
|
|
|
1; |
73
|
|
|
|
|
|
|
|
74
|
|
|
|
|
|
|
__END__ |
75
|
|
|
|
|
|
|
|
76
|
|
|
|
|
|
|
=pod |
77
|
|
|
|
|
|
|
|
78
|
|
|
|
|
|
|
=encoding UTF-8 |
79
|
|
|
|
|
|
|
|
80
|
|
|
|
|
|
|
=head1 NAME |
81
|
|
|
|
|
|
|
|
82
|
|
|
|
|
|
|
Language::FormulaEngine - Parser/Interpreter/Compiler for simple spreadsheet formula language |
83
|
|
|
|
|
|
|
|
84
|
|
|
|
|
|
|
=head1 VERSION |
85
|
|
|
|
|
|
|
|
86
|
|
|
|
|
|
|
version 0.06 |
87
|
|
|
|
|
|
|
|
88
|
|
|
|
|
|
|
=head1 SYNOPSIS |
89
|
|
|
|
|
|
|
|
90
|
|
|
|
|
|
|
my $vars= { foo => 1, bar => 3.14159265358979, baz => 42 }; |
91
|
|
|
|
|
|
|
|
92
|
|
|
|
|
|
|
my $engine= Language::FormulaEngine->new(); |
93
|
|
|
|
|
|
|
$engine->evaluate( 'if(foo, round(bar, 3), baz*100)', $vars ); |
94
|
|
|
|
|
|
|
|
95
|
|
|
|
|
|
|
# or for more speed on repeat evaluations |
96
|
|
|
|
|
|
|
my $formula= $engine->compile( 'if(foo, round(bar, 3), baz*100)' ); |
97
|
|
|
|
|
|
|
print $formula->($vars); |
98
|
|
|
|
|
|
|
|
99
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
package MyNamespace { |
101
|
|
|
|
|
|
|
use Moo; |
102
|
|
|
|
|
|
|
extends 'Language::FormulaEngine::Namespace::Default'; |
103
|
|
|
|
|
|
|
sub fn_customfunc { print "arguments are ".join(', ', @_)."\n"; } |
104
|
|
|
|
|
|
|
}; |
105
|
|
|
|
|
|
|
my $engine= Language::FormulaEngine->new(namespace => MyNamespace->new); |
106
|
|
|
|
|
|
|
my $formula= $engine->compile( 'CustomFunc(baz,2,3)' ); |
107
|
|
|
|
|
|
|
$formula->($vars); # prints "arguments are 42, 2, 3\n" |
108
|
|
|
|
|
|
|
|
109
|
|
|
|
|
|
|
=head1 DESCRIPTION |
110
|
|
|
|
|
|
|
|
111
|
|
|
|
|
|
|
This set of modules implement a parser, evaluator, and optional code generator for a simple |
112
|
|
|
|
|
|
|
expression language similar to those used in spreadsheets. |
113
|
|
|
|
|
|
|
The intent of this module is to help you add customizable behavior to your applications that an |
114
|
|
|
|
|
|
|
"office power-user" can quickly learn and use, while also not opening up security holes in your |
115
|
|
|
|
|
|
|
application. |
116
|
|
|
|
|
|
|
|
117
|
|
|
|
|
|
|
In a typical business application, there will always be another few use cases that the customer |
118
|
|
|
|
|
|
|
didn't know about or think to tell you about, and adding support for these use cases can result |
119
|
|
|
|
|
|
|
in a never-ending expansion of options and chekboxes and dropdowns, and a lot of time spent |
120
|
|
|
|
|
|
|
deciding the logical way for them to all interact. |
121
|
|
|
|
|
|
|
One way to solve this is to provide some scripting support for the customer to use. However, |
122
|
|
|
|
|
|
|
you want to make the language easy to learn, "nerfed" enough for them to use safely, and |
123
|
|
|
|
|
|
|
prevent security vulnerabilities. The challenge is finding a language that they find familiar, |
124
|
|
|
|
|
|
|
that is easy to write correct programs with, and that dosn't expose any peice of the system |
125
|
|
|
|
|
|
|
that you didn't intend to expose. I chose "spreadsheet formula language" for a project back in |
126
|
|
|
|
|
|
|
2012 and it worked out really well, so I decided to give it a makeover and publish it. |
127
|
|
|
|
|
|
|
|
128
|
|
|
|
|
|
|
The default syntax is pure-functional, in that each operation has exactly one return value, and |
129
|
|
|
|
|
|
|
cannot modify variables; in fact none of the default functions have any side-effects. There is |
130
|
|
|
|
|
|
|
no assignment, looping, or nested data structures. The language does have a bit of a Perl twist |
131
|
|
|
|
|
|
|
to it's semantics, like throwing exceptions rather than returning C<< #VALUE! >>, fluidly |
132
|
|
|
|
|
|
|
interpreting values as strings or integers, and using L<DateTime> instead of days-since-1900 |
133
|
|
|
|
|
|
|
numbers for dates, but most users probably won't mind. And, all these decisions are fairly |
134
|
|
|
|
|
|
|
easy to change with a subclass. |
135
|
|
|
|
|
|
|
(but if you want big changes, you should L<review your options|/"SEE ALSO"> to make sure you're |
136
|
|
|
|
|
|
|
starting with the right module.) |
137
|
|
|
|
|
|
|
|
138
|
|
|
|
|
|
|
The language is written with security in mind, and (until you start making changes) |
139
|
|
|
|
|
|
|
should be safe for most uses, since the functional design promotes O(1) complexity |
140
|
|
|
|
|
|
|
and shouldn't have side effects on the data structures you expose to the user. |
141
|
|
|
|
|
|
|
The optional L</compile> method does use C<eval> though, so you should do an audit for |
142
|
|
|
|
|
|
|
yourself if you plan to use it where security is a concern. |
143
|
|
|
|
|
|
|
|
144
|
|
|
|
|
|
|
B<Features:> |
145
|
|
|
|
|
|
|
|
146
|
|
|
|
|
|
|
=over |
147
|
|
|
|
|
|
|
|
148
|
|
|
|
|
|
|
=item * |
149
|
|
|
|
|
|
|
|
150
|
|
|
|
|
|
|
Standard design with scanner/parser, syntax tree, namespaces, and compiler. |
151
|
|
|
|
|
|
|
|
152
|
|
|
|
|
|
|
=item * |
153
|
|
|
|
|
|
|
|
154
|
|
|
|
|
|
|
Can compile to perl coderefs for fast repeated execution |
155
|
|
|
|
|
|
|
|
156
|
|
|
|
|
|
|
=item * |
157
|
|
|
|
|
|
|
|
158
|
|
|
|
|
|
|
Provides metadata about what it compiled |
159
|
|
|
|
|
|
|
|
160
|
|
|
|
|
|
|
=item * |
161
|
|
|
|
|
|
|
|
162
|
|
|
|
|
|
|
Designed for extensibility |
163
|
|
|
|
|
|
|
|
164
|
|
|
|
|
|
|
=item * |
165
|
|
|
|
|
|
|
|
166
|
|
|
|
|
|
|
Light-weight, few dependencies, clean code |
167
|
|
|
|
|
|
|
|
168
|
|
|
|
|
|
|
=item * |
169
|
|
|
|
|
|
|
|
170
|
|
|
|
|
|
|
Recursive-descent parse, which is easier to work with and gives helpful error messages, |
171
|
|
|
|
|
|
|
though could get a bit slow if you extend the grammar too much. |
172
|
|
|
|
|
|
|
(for simple grammars like this, it's pretty fast) |
173
|
|
|
|
|
|
|
|
174
|
|
|
|
|
|
|
=back |
175
|
|
|
|
|
|
|
|
176
|
|
|
|
|
|
|
=head1 ATTRIBUTES |
177
|
|
|
|
|
|
|
|
178
|
|
|
|
|
|
|
=head2 parser |
179
|
|
|
|
|
|
|
|
180
|
|
|
|
|
|
|
A parser for the language. Responsible for tokenizing the input and building the |
181
|
|
|
|
|
|
|
parse tree. |
182
|
|
|
|
|
|
|
|
183
|
|
|
|
|
|
|
Defaults to an instance of L<Language::FormulaEngine::Parser>. You can initialize this |
184
|
|
|
|
|
|
|
attribute with an object instance, a class name, or arguments for the default parser. |
185
|
|
|
|
|
|
|
|
186
|
|
|
|
|
|
|
=head2 namespace |
187
|
|
|
|
|
|
|
|
188
|
|
|
|
|
|
|
A namespace for looking up functions or constants. Also determines some aspects of how the |
189
|
|
|
|
|
|
|
language works, and responsible for providing the perl code when compiling expressions. |
190
|
|
|
|
|
|
|
|
191
|
|
|
|
|
|
|
Defaults to an instance of L<Language::FormulaEngine::Namespace::Default>. |
192
|
|
|
|
|
|
|
You can initialize this with an object instance, class name, version number for the default |
193
|
|
|
|
|
|
|
namespace, or hashref of arguments for the constructor. |
194
|
|
|
|
|
|
|
|
195
|
|
|
|
|
|
|
=head2 compiler |
196
|
|
|
|
|
|
|
|
197
|
|
|
|
|
|
|
A compiler for the parse tree. Responsible for generating Perl coderefs, though the Namespace |
198
|
|
|
|
|
|
|
does most of the perl code generation. |
199
|
|
|
|
|
|
|
|
200
|
|
|
|
|
|
|
Defaults to an instance of L<Language::FormulaEngine::Compiler>. |
201
|
|
|
|
|
|
|
You can initialize this attribute with a class instance, a class name, or arguments for the |
202
|
|
|
|
|
|
|
default compiler. |
203
|
|
|
|
|
|
|
|
204
|
|
|
|
|
|
|
=head1 METHODS |
205
|
|
|
|
|
|
|
|
206
|
|
|
|
|
|
|
=head2 evaluate |
207
|
|
|
|
|
|
|
|
208
|
|
|
|
|
|
|
my $value= $fe->evaluate( $formula_text, \%variables ); |
209
|
|
|
|
|
|
|
|
210
|
|
|
|
|
|
|
This method creates a new namespace from the default plus the supplied variables, parses the |
211
|
|
|
|
|
|
|
formula, then evaluates it in a recursive interpreted manner, returning the result. Exceptions |
212
|
|
|
|
|
|
|
may be thrown during parsing or execution. |
213
|
|
|
|
|
|
|
|
214
|
|
|
|
|
|
|
=head2 compile |
215
|
|
|
|
|
|
|
|
216
|
|
|
|
|
|
|
my $coderef= $fe->compile( $formula_text ); |
217
|
|
|
|
|
|
|
|
218
|
|
|
|
|
|
|
Parses and then compiles the C<$formula_text>, returning a coderef. Exceptions may be thrown |
219
|
|
|
|
|
|
|
during parsing or execution. |
220
|
|
|
|
|
|
|
|
221
|
|
|
|
|
|
|
=head1 CUSTOMIZING THE LANGUAGE |
222
|
|
|
|
|
|
|
|
223
|
|
|
|
|
|
|
The module is called "FormulaEngine" in part because it is designed to be customized. |
224
|
|
|
|
|
|
|
The functions are easy to extend, the variables are somewhat easy to extend, the compilation |
225
|
|
|
|
|
|
|
can be extended after a little study of the API, and the grammar itself can be extended with |
226
|
|
|
|
|
|
|
some effort. |
227
|
|
|
|
|
|
|
|
228
|
|
|
|
|
|
|
If you are trying to addd I<lots> of functionality, you might be starting with the wrong module. |
229
|
|
|
|
|
|
|
See the notes in the L</"SEE ALSO"> section. |
230
|
|
|
|
|
|
|
|
231
|
|
|
|
|
|
|
=head2 Adding Functions |
232
|
|
|
|
|
|
|
|
233
|
|
|
|
|
|
|
The easiest thing to extend is the namespace of available functions. Just subclass |
234
|
|
|
|
|
|
|
L<Language::FormulaEngine::Namespace> and add the functions you want starting with the prefix |
235
|
|
|
|
|
|
|
C<fn_>. |
236
|
|
|
|
|
|
|
|
237
|
|
|
|
|
|
|
=head2 Complex Variables |
238
|
|
|
|
|
|
|
|
239
|
|
|
|
|
|
|
The default implementation of Namespace requires all variables to be stored in a single hashref. |
240
|
|
|
|
|
|
|
This default is safe and fast. If you want to traverse nested data structures or call methods, |
241
|
|
|
|
|
|
|
you also need to subclass L<Language::FormulaEngine::Namespace/get_value>. |
242
|
|
|
|
|
|
|
|
243
|
|
|
|
|
|
|
=head2 Changing Semantics |
244
|
|
|
|
|
|
|
|
245
|
|
|
|
|
|
|
The namespace is also in control of the behavior of the functions and operators (which are |
246
|
|
|
|
|
|
|
themselves just functions). It controls both the way they are evaluated and the perl code they |
247
|
|
|
|
|
|
|
generate if compiled. |
248
|
|
|
|
|
|
|
|
249
|
|
|
|
|
|
|
=head2 Adding New Operators |
250
|
|
|
|
|
|
|
|
251
|
|
|
|
|
|
|
If you want to make small changes to the grammar, such as adding new prefix/suffix/infix |
252
|
|
|
|
|
|
|
operators, this can be accomplished fairly easily by subclassing the Parser. The parser just |
253
|
|
|
|
|
|
|
returns trees of functions, and if you look at the pattern used in the recursive descent |
254
|
|
|
|
|
|
|
C<parse_*> methods it should be easy to add some new ones. |
255
|
|
|
|
|
|
|
|
256
|
|
|
|
|
|
|
=head2 Bigger Grammar Changes |
257
|
|
|
|
|
|
|
|
258
|
|
|
|
|
|
|
Any customization involving bigger changes to the grammar, like adding assignments or multi- |
259
|
|
|
|
|
|
|
statement blocks or map/reduce, would require a bigger rewrite. Consider starting with |
260
|
|
|
|
|
|
|
a different more powerful parsing system for that. |
261
|
|
|
|
|
|
|
|
262
|
|
|
|
|
|
|
=head1 SEE ALSO |
263
|
|
|
|
|
|
|
|
264
|
|
|
|
|
|
|
=over |
265
|
|
|
|
|
|
|
|
266
|
|
|
|
|
|
|
=item L<Language::Expr> |
267
|
|
|
|
|
|
|
|
268
|
|
|
|
|
|
|
A bigger more featureful expression language; perl-like syntax and data structures. |
269
|
|
|
|
|
|
|
Also much more complicated and harder to customize. |
270
|
|
|
|
|
|
|
Can also compile to Javascript! |
271
|
|
|
|
|
|
|
|
272
|
|
|
|
|
|
|
=item L<Math::Expression> |
273
|
|
|
|
|
|
|
|
274
|
|
|
|
|
|
|
General-purpose language, including variable assignment and loops, arrays, |
275
|
|
|
|
|
|
|
and with full attention to security. However, grammar is not customizable at all, |
276
|
|
|
|
|
|
|
and math-centric. |
277
|
|
|
|
|
|
|
|
278
|
|
|
|
|
|
|
=item L<Math::Expression::Evaluator> |
279
|
|
|
|
|
|
|
|
280
|
|
|
|
|
|
|
Very similar to this module, but no string support and not very customizable. |
281
|
|
|
|
|
|
|
Supports assignments, and compilation. |
282
|
|
|
|
|
|
|
|
283
|
|
|
|
|
|
|
=item L<Math::Expr> |
284
|
|
|
|
|
|
|
|
285
|
|
|
|
|
|
|
Similar expression parser, but without string support. |
286
|
|
|
|
|
|
|
Supports arbitrary customization of operators. |
287
|
|
|
|
|
|
|
Not suitable for un-trusted strings, according to BUGS documentation. |
288
|
|
|
|
|
|
|
|
289
|
|
|
|
|
|
|
=back |
290
|
|
|
|
|
|
|
|
291
|
|
|
|
|
|
|
=head1 AUTHOR |
292
|
|
|
|
|
|
|
|
293
|
|
|
|
|
|
|
Michael Conrad <mconrad@intellitree.com> |
294
|
|
|
|
|
|
|
|
295
|
|
|
|
|
|
|
=head1 COPYRIGHT AND LICENSE |
296
|
|
|
|
|
|
|
|
297
|
|
|
|
|
|
|
This software is copyright (c) 2021 by Michael Conrad, IntelliTree Solutions llc. |
298
|
|
|
|
|
|
|
|
299
|
|
|
|
|
|
|
This is free software; you can redistribute it and/or modify it under |
300
|
|
|
|
|
|
|
the same terms as the Perl 5 programming language system itself. |
301
|
|
|
|
|
|
|
|
302
|
|
|
|
|
|
|
=cut |