File Coverage

blib/lib/Language/Expr/Compiler/js.pm
Criterion Covered Total %
statement 53 338 15.6
branch 9 150 6.0
condition 2 25 8.0
subroutine 11 40 27.5
pod 1 32 3.1
total 76 585 12.9


line stmt bran cond sub pod time code
1             package Language::Expr::Compiler::js;
2              
3             our $DATE = '2016-06-29'; # DATE
4             our $VERSION = '0.27'; # VERSION
5              
6 1     1   26 use 5.010;
  1         3  
7 1     1   4 use strict;
  1         1  
  1         20  
8 1     1   3 use warnings;
  1         2  
  1         23  
9              
10 1     1   3 use Role::Tiny::With;
  1         1  
  1         55  
11 1     1   3 use parent 'Language::Expr::Compiler::Base';
  1         1  
  1         6  
12             with 'Language::Expr::CompilerRole';
13              
14             sub rule_pair_simple {
15 0     0 0 0 my ($self, %args) = @_;
16 0         0 my $match = $args{match};
17 0         0 "'$match->{key}':$match->{value}";
18             }
19              
20             sub rule_pair_string {
21 0     0 0 0 my ($self, %args) = @_;
22 0         0 my $match = $args{match};
23 0         0 "$match->{key}:$match->{value}";
24             }
25              
26             sub rule_or_xor {
27 0     0 0 0 my ($self, %args) = @_;
28 0         0 my $match = $args{match};
29 0         0 my @res;
30 0         0 push @res, shift @{$match->{operand}};
  0         0  
31 0         0 for my $term (@{$match->{operand}}) {
  0         0  
32 0   0     0 my $op = shift @{$match->{op}//=[]};
  0         0  
33 0 0       0 last unless $op;
34 0 0       0 if ($op eq '||') { push @res, " || $term" }
  0 0       0  
    0          
35 0         0 elsif ($op eq '//') { @res = ("(function() { let _x = (",
36             @res, "); return _x==null ? (",
37             $term, ") : _x })()") }
38 0         0 elsif ($op eq '^^') { @res = ("(function() { let _a = (",
39             @res, "); let _b = ($term); ",
40             "return _a&&!_b || !_a&&_b ? _a : _b })()") }
41             }
42 0         0 join "", grep {defined} @res;
  0         0  
43             }
44              
45             sub rule_and {
46 0     0 0 0 my ($self, %args) = @_;
47 0         0 my $match = $args{match};
48 0         0 my @res;
49 0         0 push @res, shift @{$match->{operand}};
  0         0  
50 0         0 for my $term (@{$match->{operand}}) {
  0         0  
51 0   0     0 my $op = shift @{$match->{op}//=[]};
  0         0  
52 0 0       0 last unless $op;
53 0 0       0 if ($op eq '&&') { push @res, " && $term" }
  0         0  
54             }
55 0         0 join "", grep {defined} @res;
  0         0  
56             }
57              
58             sub rule_ternary {
59 0     0 0 0 my ($self, %args) = @_;
60 0         0 my $match = $args{match};
61 0         0 my $opd = $match->{operand};
62 0         0 "($opd->[0] ? $opd->[1] : $opd->[2])";
63             }
64              
65             sub rule_bit_or_xor {
66 0     0 0 0 my ($self, %args) = @_;
67 0         0 my $match = $args{match};
68 0         0 my @res;
69 0         0 push @res, shift @{$match->{operand}};
  0         0  
70 0         0 for my $term (@{$match->{operand}}) {
  0         0  
71 0   0     0 my $op = shift @{$match->{op}//=[]};
  0         0  
72 0 0       0 last unless $op;
73 0 0       0 if ($op eq '|') { push @res, " | $term" }
  0 0       0  
74 0         0 elsif ($op eq '^') { push @res, " ^ $term" }
75             }
76 0         0 join "", grep {defined} @res;
  0         0  
77             }
78              
79             sub rule_bit_and {
80 0     0 0 0 my ($self, %args) = @_;
81 0         0 my $match = $args{match};
82 0         0 my @res;
83 0         0 push @res, shift @{$match->{operand}};
  0         0  
84 0         0 for my $term (@{$match->{operand}}) {
  0         0  
85 0   0     0 my $op = shift @{$match->{op}//=[]};
  0         0  
86 0 0       0 last unless $op;
87 0 0       0 if ($op eq '&') { push @res, " & $term" }
  0         0  
88             }
89 0         0 join "", grep {defined} @res;
  0         0  
90             }
91              
92             sub rule_comparison3 {
93 0     0 0 0 my ($self, %args) = @_;
94 0         0 my $match = $args{match};
95 0         0 my @res;
96 0         0 push @res, shift @{$match->{operand}};
  0         0  
97 0         0 for my $term (@{$match->{operand}}) {
  0         0  
98 0   0     0 my $op = shift @{$match->{op}//=[]};
  0         0  
99 0 0       0 last unless $op;
100 0 0       0 if ($op eq '<=>') { @res = ("(function() { let _a = (",
  0 0       0  
101             @res, "); let _b = ($term); ",
102             "return _a > _b ? 1 : (_a < _b ? -1 : 0) })()") }
103 0         0 elsif ($op eq 'cmp') { @res = ("(function() { let _a = (",
104             @res, ") + ''; let _b = ($term) + ''; ",
105             "return _a > _b ? 1 : (_a < _b ? -1 : 0) })()") }
106             }
107 0         0 join "", grep {defined} @res;
  0         0  
108             }
109              
110             sub _comparison1 {
111 0     0   0 my ($opd1, $op, $opd2) = @_;
112 0 0       0 if ($op eq 'eq') { return "(function() { let _a = ($opd1) + ''; let _b = ($opd2) + ''; return $opd1 == $opd2 })()" }
  0 0       0  
    0          
    0          
    0          
    0          
113 0         0 elsif ($op eq 'ne') { return "(function() { let _a = ($opd1) + ''; let _b = ($opd2) + ''; return $opd1 != $opd2 })()" }
114 0         0 elsif ($op eq 'lt') { return "(function() { let _a = ($opd1) + ''; let _b = ($opd2) + ''; return $opd1 < $opd2 })()" }
115 0         0 elsif ($op eq 'le') { return "(function() { let _a = ($opd1) + ''; let _b = ($opd2) + ''; return $opd1 <= $opd2 })()" }
116 0         0 elsif ($op eq 'gt') { return "(function() { let _a = ($opd1) + ''; let _b = ($opd2) + ''; return $opd1 > $opd2 })()" }
117 0         0 elsif ($op eq 'ge') { return "(function() { let _a = ($opd1) + ''; let _b = ($opd2) + ''; return $opd1 >= $opd2 })()" }
118 0         0 else { return "($opd1 $op $opd2)" }
119             }
120              
121             sub rule_comparison {
122 0     0 0 0 my ($self, %args) = @_;
123 0         0 my $match = $args{match};
124 0         0 my @opds;
125 0         0 push @opds, shift @{$match->{operand}};
  0         0  
126 0 0       0 return '' unless defined $opds[0];
127 0         0 my @ops;
128 0         0 for my $term (@{$match->{operand}}) {
  0         0  
129 0         0 push @opds, $term;
130 0   0     0 my $op = shift @{$match->{op}//=[]};
  0         0  
131 0 0       0 last unless $op;
132 0 0       0 if ($op eq '==' ) { push @ops, '==' }
  0 0       0  
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
133 0         0 elsif ($op eq '!=' ) { push @ops, '!=' }
134 0         0 elsif ($op eq 'eq' ) { push @ops, 'eq' }
135 0         0 elsif ($op eq 'ne' ) { push @ops, 'ne' }
136 0         0 elsif ($op eq '<' ) { push @ops, '<' }
137 0         0 elsif ($op eq '<=' ) { push @ops, '<=' }
138 0         0 elsif ($op eq '>' ) { push @ops, '>' }
139 0         0 elsif ($op eq '>=' ) { push @ops, '>=' }
140 0         0 elsif ($op eq 'lt' ) { push @ops, 'lt' }
141 0         0 elsif ($op eq 'le' ) { push @ops, 'le' }
142 0         0 elsif ($op eq 'gt' ) { push @ops, 'gt' }
143 0         0 elsif ($op eq 'ge' ) { push @ops, 'ge' }
144             }
145 0 0       0 return $opds[0] unless @ops;
146 0         0 my @res;
147             my $lastopd;
148 0         0 my ($opd1, $opd2);
149 0         0 while (@ops) {
150 0         0 my $op = pop @ops;
151 0 0       0 if (defined($lastopd)) {
152 0         0 $opd2 = $lastopd;
153 0         0 $opd1 = pop @opds;
154             } else {
155 0         0 $opd2 = pop @opds;
156 0         0 $opd1 = pop @opds;
157             }
158 0 0       0 if (@res) {
159 0         0 @res = ("("._comparison1($opd1, $op, $opd2)." ? ", @res, " : false)");
160             } else {
161 0         0 push @res, _comparison1($opd1, $op, $opd2);
162             }
163 0         0 $lastopd = $opd1;
164             }
165 0         0 join "", @res;
166             }
167              
168             sub rule_bit_shift {
169 0     0 0 0 my ($self, %args) = @_;
170 0         0 my $match = $args{match};
171 0         0 my @res;
172 0         0 push @res, shift @{$match->{operand}};
  0         0  
173 0         0 for my $term (@{$match->{operand}}) {
  0         0  
174 0   0     0 my $op = shift @{$match->{op}//=[]};
  0         0  
175 0 0       0 last unless $op;
176 0 0       0 if ($op eq '>>') { push @res, " >> $term" }
  0 0       0  
177 0         0 elsif ($op eq '<<') { push @res, " << $term" }
178             }
179 0         0 join "", grep {defined} @res;
  0         0  
180             }
181              
182             sub rule_add {
183 1     1 0 3 my ($self, %args) = @_;
184 1         1 my $match = $args{match};
185 1         2 my @res;
186 1         1 push @res, shift @{$match->{operand}};
  1         2  
187 1         2 for my $term (@{$match->{operand}}) {
  1         2  
188 1   50     1 my $op = shift @{$match->{op}//=[]};
  1         4  
189 1 50       2 last unless $op;
190 1 50       4 if ($op eq '.') { @res = ("'' + ", @res, " + $term") }
  1         6  
191 1 50       3 if ($op eq '+') { push @res, " + $term" }
  0         0  
192 1 50       2 if ($op eq '-') { push @res, " - $term" }
  0         0  
193             }
194 1         2 join "", grep {defined} @res;
  3         25  
195             }
196              
197             sub rule_mult {
198 0     0 0 0 my ($self, %args) = @_;
199 0         0 my $match = $args{match};
200 0         0 my @res;
201 0         0 push @res, shift @{$match->{operand}};
  0         0  
202 0         0 for my $term (@{$match->{operand}}) {
  0         0  
203 0   0     0 my $op = shift @{$match->{op}//=[]};
  0         0  
204 0 0       0 last unless $op;
205 0 0       0 if ($op eq '*') { push @res, " * $term" }
  0         0  
206 0 0       0 if ($op eq '/') { push @res, " / $term" }
  0         0  
207 0 0       0 if ($op eq '%') { push @res, " % $term" }
  0         0  
208 0 0       0 if ($op eq 'x') { @res = ("(new Array(1 + $term).join(", @res, "))") }
  0         0  
209             }
210 0         0 join "", grep {defined} @res;
  0         0  
211             }
212              
213             sub rule_unary {
214 0     0 0 0 my ($self, %args) = @_;
215 0         0 my $match = $args{match};
216 0         0 my @res;
217 0         0 push @res, $match->{operand};
218 0   0     0 for my $op (reverse @{$match->{op}//=[]}) {
  0         0  
219 0 0       0 last unless $op;
220             # use paren because --x or ++x is interpreted as pre-decrement/increment
221 0 0       0 if ($op eq '!') { @res = ("!(", @res, ")") }
  0         0  
222 0 0       0 if ($op eq '-') { @res = ("-(", @res, ")") }
  0         0  
223 0 0       0 if ($op eq '~') { @res = ("~(", @res, ")") }
  0         0  
224             }
225 0         0 join "", grep {defined} @res;
  0         0  
226             }
227              
228             sub rule_power {
229 0     0 0 0 my ($self, %args) = @_;
230 0         0 my $match = $args{match};
231 0         0 my @res;
232 0         0 push @res, pop @{$match->{operand}};
  0         0  
233 0         0 for my $term (reverse @{$match->{operand}}) {
  0         0  
234 0         0 @res = ("Math.pow($term, ", @res, ")");
235             }
236 0         0 join "", grep {defined} @res;
  0         0  
237             }
238              
239             sub rule_subscripting_var {
240 0     0 0 0 my ($self, %args) = @_;
241 0         0 $self->rule_subscripting_expr(%args);
242             }
243              
244             sub rule_subscripting_expr {
245 0     0 0 0 my ($self, %args) = @_;
246 0         0 my $match = $args{match};
247 0         0 my $opd = $match->{operand};
248 0   0     0 my @ss = @{$match->{subscript}//=[]};
  0         0  
249 0 0       0 return $opd unless @ss;
250 0         0 my $res;
251 0         0 for my $s (@ss) {
252 0 0       0 $opd = $res if defined($res);
253 0         0 $res = $opd . "[$s]";
254             }
255 0         0 $res;
256             }
257              
258             sub rule_array {
259 0     0 0 0 my ($self, %args) = @_;
260 0         0 my $match = $args{match};
261 0         0 "[" . join(", ", @{ $match->{element} }) . "]";
  0         0  
262             }
263              
264             sub rule_hash {
265 0     0 0 0 my ($self, %args) = @_;
266 0         0 my $match = $args{match};
267 0         0 "{" . join(", ", @{ $match->{pair} }). "}";
  0         0  
268             }
269              
270             sub rule_undef {
271 0     0 0 0 "null";
272             }
273              
274             sub rule_squotestr {
275 0     0 0 0 my ($self, %args) = @_;
276             join(" + ",
277 0         0 map { $self->_quote($_->{value}) }
278 0         0 @{ $self->parse_squotestr($args{match}{part}) });
  0         0  
279             }
280              
281             sub rule_dquotestr {
282 2     2 0 4 my ($self, %args) = @_;
283             my @tmp =
284             map { $_->{type} eq 'VAR' ?
285             $self->rule_var(match=>{var=>$_->{value}}) :
286             $self->_quote($_->{value})
287 2 50       8 }
288 2         2 @{ $self->parse_dquotestr($args{match}{part}) };
  2         7  
289 2 50       6 if (@tmp > 1) {
290 0         0 "(". join(" + ", @tmp) . ")[0]";
291             } else {
292 2         47 $tmp[0];
293             }
294             }
295              
296             sub rule_bool {
297 0     0 0 0 my ($self, %args) = @_;
298 0         0 my $match = $args{match};
299 0 0       0 if ($match->{bool} eq 'true') { "true" } else { "false" }
  0         0  
  0         0  
300             }
301              
302             sub rule_num {
303 0     0 0 0 my ($self, %args) = @_;
304 0         0 my $match = $args{match};
305 0 0       0 if ($match->{num} eq 'inf') { 'Infinity' }
  0 0       0  
306 0         0 elsif ($match->{num} eq 'nan') { 'NaN' }
307 0         0 else { $match->{num}+0 }
308             }
309              
310             sub rule_var {
311 0     0 0 0 my ($self, %args) = @_;
312 0         0 my $match = $args{match};
313 0 0       0 if ($self->hook_var) {
314 0         0 my $res = $self->hook_var->($match->{var});
315 0 0       0 return $res if defined($res);
316             }
317 0         0 return "$match->{var}";
318             }
319              
320             sub rule_func {
321 0     0 0 0 my ($self, %args) = @_;
322 0         0 my $match = $args{match};
323 0         0 my $f = $match->{func_name};
324 0         0 my $args = $match->{args};
325 0 0       0 if ($self->hook_func) {
326 0         0 my $res = $self->hook_func->($f, @$args);
327 0 0       0 return $res if defined($res);
328             }
329 0         0 my $fmap = $self->func_mapping->{$f};
330 0 0       0 $f = $fmap if $fmap;
331 0         0 my $fc = substr($f, 0, 1);
332 0 0       0 if ($fc eq '.') {
    0          
333 0         0 my $invoc = shift @$args;
334 0         0 return "($invoc)$f(".join(", ", @$args).")";
335             } elsif ($fc eq ':') {
336 0         0 my $invoc = shift @$args;
337 0         0 my $prop = substr($f, 1, length($f)-1);
338 0         0 return "($invoc).$prop";
339             } else {
340 0         0 return "$f(".join(", ", @$args).")";
341             }
342             }
343              
344             sub _map_grep_usort {
345 0     0   0 my ($self, $which, %args) = @_;
346 0         0 my $match = $args{match};
347 0         0 my $ary = $match->{array};
348 0         0 my $expr = $match->{expr};
349              
350 0         0 my $uuid = $self->new_marker('subexpr', $expr);
351 0 0       0 if ($which eq 'map') {
    0          
    0          
352 0         0 return "($ary).map(function(_){ return (TODO-$uuid); })";
353             } elsif ($which eq 'grep') {
354 0         0 return "($ary).filter(function(_){ return (TODO-$uuid); })";
355             } elsif ($which eq 'usort') {
356 0         0 return "($ary).sort(function(a, b){ return (TODO-$uuid); })";
357             }
358             }
359              
360             sub rule_func_map {
361 0     0 0 0 my ($self, %args) = @_;
362 0         0 $self->_map_grep_usort('map', %args);
363             }
364              
365             sub rule_func_grep {
366 0     0 0 0 my ($self, %args) = @_;
367 0         0 $self->_map_grep_usort('grep', %args);
368             }
369              
370             sub rule_func_usort {
371 0     0 0 0 my ($self, %args) = @_;
372 0         0 $self->_map_grep_usort('usort', %args);
373             }
374              
375             sub rule_parenthesis {
376 0     0 0 0 my ($self, %args) = @_;
377 0         0 my $match = $args{match};
378 0         0 "(" . $match->{answer} . ")";
379             }
380              
381       1 0   sub expr_preprocess {}
382              
383             sub expr_postprocess {
384 1     1 0 3 my ($self, %args) = @_;
385 1         1 my $result = $args{result};
386 1         3 $result;
387             }
388              
389             # can't use regex here (perl segfaults), at least in 5.10.1, because
390             # we are in one big re::gr regex.
391             sub _quote {
392 2     2   2 my ($self, $str) = @_;
393 2         1 my @c;
394 2         5 for my $c (split '', $str) {
395 2         2 my $o = ord($c);
396 2 50 33     12 if ($c eq '"') { push @c, '\\"' }
  0 50       0  
    50          
    0          
397 0         0 elsif ($c eq "\\") { push @c, "\\\\" }
398 2         3 elsif ($o >= 32 && $o <= 127) { push @c, $c }
399 0         0 elsif ($o > 255) { push @c, sprintf("\\u%04x", $o) }
400 0         0 else { push @c, sprintf("\\x%02x", $o) }
401             }
402 2         6 '"' . join("", @c) . '"';
403             }
404              
405             sub compile {
406 1     1 1 49 require Language::Expr::Parser;
407              
408 1         2 my ($self, $expr) = @_;
409 1         3 my $res = Language::Expr::Parser::parse_expr($expr, $self);
410 1         1 for my $m (@{ $self->markers }) {
  1         8  
411 0         0 my $type = $m->[0];
412 0 0       0 next unless $type eq 'subexpr';
413 0         0 my $uuid = $m->[1];
414 0         0 my $subexpr = $m->[2];
415 0         0 my $subres = Language::Expr::Parser::parse_expr($subexpr, $self);
416 0         0 $res =~ s/TODO-$uuid/$subres/g;
417             }
418 1         2 $self->markers([]);
419 1         10 $res;
420             }
421              
422             1;
423             # ABSTRACT: Compile Language::Expr expression to JavaScript
424              
425             __END__
426              
427             =pod
428              
429             =encoding UTF-8
430              
431             =head1 NAME
432              
433             Language::Expr::Compiler::js - Compile Language::Expr expression to JavaScript
434              
435             =head1 VERSION
436              
437             This document describes version 0.27 of Language::Expr::Compiler::js (from Perl distribution Language-Expr), released on 2016-06-29.
438              
439             =head1 SYNOPSIS
440              
441             use Language::Expr::Compiler::js;
442             my $jsc = Language::Expr::Compiler::js->new;
443             print $jsc->compile('map({$_**2}, [1, 2, 3])'); # prints: [1, 2, 3].map(function(_){ Math.pow(_, 2) })
444              
445             # map Expr function to JS function/method/property
446             $jsc->func_mapping->{ceil} = 'Math.ceil';
447             $jsc->func_mapping->{uc} = '.toUpperCase';
448             $jsc->func_mapping->{length} = ':length';
449             print $jsc->compile(q{uc("party like it's ") . ceil(1998.9)}); # prints: "party like it's ".toUpperCase() + Math.ceil(1998.9)
450              
451             =head1 DESCRIPTION
452              
453             Compiles Language::Expr expression to JavaScript code. Some notes:
454              
455             =over 4
456              
457             =item * JavaScript version
458              
459             This compiler emits JavaScript 1.8.1 code (it uses several Array methods like
460             map() and filter() [supported since JavaScript 1.6], 'let' lexical variables
461             [supported since JavaScript 1.7] and native JSON [supported since JavaScript
462             1.8.1]).
463              
464             To test emitted JavaScript code, we use Node.js.
465              
466             =item * JavaScript-ness
467              
468             The emitted JS code will follow JavaScript's weak typing and coercion rules,
469             e.g. Expr '1+"2"' will simply be translated to JavaScript '1+"2"' and will
470             result in "12".
471              
472             =item * Variables by default simply use JavaScript variables.
473              
474             E.g. $a becomes a, and so on. Be careful not to make variables which are invalid
475             in JavaScript, e.g. $.. or ${foo/bar}.
476              
477             You can customize this behaviour by subclassing rule_var() or by providing a
478             hook_var() (see documentation in L<Language::Expr::Compiler::Base>).
479              
480             =item * Functions by default simply use Javascript functions.
481              
482             Except those mentioned in B<func_mapping> (e.g. rand() becomes Math.rand() if
483             func_mapping->{rand} is 'Math.rand'). You can also map to JavaScript method
484             (using '.meth' syntax) and property (using ':prop' syntax).
485              
486             You can customize this behaviour by subclassing rule_func() or by providing a
487             hook_func() (see documentation in L<Language::Expr::Compiler::Base>).
488              
489             =back
490              
491             =for Pod::Coverage ^(rule|expr)_.+
492              
493             =head1 METHODS
494              
495             =head2 compile($expr) => $js_code
496              
497             Convert Language::Expr expression into JavaScript code. Dies if there is syntax
498             error in expression.
499              
500             =head1 HOMEPAGE
501              
502             Please visit the project's homepage at L<https://metacpan.org/release/Language-Expr>.
503              
504             =head1 SOURCE
505              
506             Source repository is at L<https://github.com/perlancar/perl-Language-Expr>.
507              
508             =head1 BUGS
509              
510             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Language-Expr>
511              
512             When submitting a bug or request, please include a test-file or a
513             patch to an existing test-file that illustrates the bug or desired
514             feature.
515              
516             =head1 AUTHOR
517              
518             perlancar <perlancar@cpan.org>
519              
520             =head1 COPYRIGHT AND LICENSE
521              
522             This software is copyright (c) 2016 by perlancar@cpan.org.
523              
524             This is free software; you can redistribute it and/or modify it under
525             the same terms as the Perl 5 programming language system itself.
526              
527             =cut