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