File Coverage

blib/lib/Sidef.pm
Criterion Covered Total %
statement 15 17 88.2
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 21 23 91.3


line stmt bran cond sub pod time code
1             package Sidef {
2              
3 1     1   49142 use 5.014;
  1         4  
4             our $VERSION = '3.05';
5              
6             our $SPACES = 0; # the current number of indentation spaces
7             our $SPACES_INCR = 4; # the number of indentation spaces
8              
9             our @NAMESPACES; # will keep track of declared modules
10             our %INCLUDED; # will keep track of included modules
11              
12             our %EVALS; # will contain info required for eval()
13              
14             use constant {
15 1         82 UPDATE_SEC => 5 * 60 * 60, # 5 hours
16             DELETE_SEC => 2 * 24 * 60 * 60, # 2 days
17             SANITIZE_SEC => 3 * 24 * 60 * 60, # 3 days
18 1     1   9 };
  1         2  
19              
20 1     1   5 use List::Util qw();
  1         2  
  1         13  
21 1     1   3 use File::Spec qw();
  1         2  
  1         15  
22              
23 1     1   288 use Sidef::Types::Bool::Bool;
  1         5  
  1         42  
24 1     1   1026 use Sidef::Types::Number::Number;
  0            
  0            
25              
26             sub new {
27             my ($class, %opt) = @_;
28             bless \%opt, $class;
29             }
30              
31             sub parse_code {
32             my ($self, $code) = @_;
33              
34             local %INCLUDED;
35             local @NAMESPACES;
36              
37             $self->{parser} //= Sidef::Parser->new(
38             opt => $self->{opt},
39             file_name => $self->{name} // '-',
40             script_name => $self->{name} // '-',
41             ($self->{parser_opt} ? (%{$self->{parser_opt}}) : ()),
42             );
43              
44             my $ast = $self->{parser}->parse_script(code => \$code);
45             $self->{namespaces} = \@NAMESPACES;
46              
47             # Check for optimization
48             if (defined(my $level = $self->{opt}{O})) {
49              
50             # Optimize the AST
51             if ($level >= 1) {
52             $ast = $self->optimize_ast($ast);
53             }
54              
55             # Deparse the AST into code, then parse the code again.
56             if ($level >= 2) {
57             my $sidef = Sidef->new(
58             opt => $self->{opt},
59             name => $self->{name},
60             parser_opt => $self->{parser_opt},
61             );
62              
63             local $sidef->{opt}{O} = 1;
64             return $sidef->parse_code($self->compile_ast($ast, 'Sidef'));
65             }
66             }
67              
68             return $ast;
69             }
70              
71             sub optimize_ast {
72             my ($self, $ast) = @_;
73             my $optimizer = Sidef::Optimizer->new;
74             scalar {$optimizer->optimize($ast)};
75             }
76              
77             sub execute_code {
78             my ($self, $code) = @_;
79             $self->execute_perl($self->compile_code($code, 'Perl'));
80             }
81              
82             sub execute_perl {
83             my ($self, $code) = @_;
84             local $Sidef::PARSER = $self->{parser};
85             local $Sidef::DEPARSER = $self->{Perl}{deparser};
86             eval($code);
87             }
88              
89             sub get_sidef_config_dir {
90             my ($self) = @_;
91             $self->{sidef_config_dir} //= $ENV{SIDEF_CONFIG_DIR}
92             || File::Spec->catdir(
93             $ENV{XDG_CONFIG_DIR}
94             || (
95             $ENV{HOME}
96             || $ENV{LOGDIR}
97             || (
98             $^O eq 'MSWin32'
99             ? '\Local Settings\Application Data'
100             : eval { ((getpwuid($<))[7] || `echo -n ~`) }
101             )
102             || File::Spec->curdir()
103             ),
104             '.config',
105             'sidef'
106             );
107             }
108              
109             sub get_sidef_vdir {
110             my ($self) = @_;
111             $self->{_sidef_vdir} //= File::Spec->catdir($self->get_sidef_config_dir, "v$VERSION");
112             }
113              
114             sub has_dbm_driver {
115             my ($self) = @_;
116              
117             if (exists $self->{dbm_driver}) {
118             return $self->{dbm_driver};
119             }
120              
121             if (eval { require DB_File; 1 }) {
122             return ($self->{dbm_driver} = 'bdbm');
123             }
124              
125             if (eval { require GDBM_File; 1 }) {
126             return ($self->{dbm_driver} = 'gdbm');
127             }
128              
129             ($self->{dbm_driver} = undef);
130             }
131              
132             sub _init_db {
133             my ($self, $hash, $db_file) = @_;
134              
135             if ($self->{dbm_driver} eq 'gdbm') {
136             require GDBM_File;
137             tie %$hash, 'GDBM_File', $db_file, &GDBM_File::GDBM_WRCREAT, 0640;
138             }
139             elsif ($self->{dbm_driver} eq 'bdbm') {
140             require DB_File;
141             require Fcntl;
142             tie %$hash, 'DB_File', $db_file, &Fcntl::O_CREAT | &Fcntl::O_RDWR, 0640, $DB_File::DB_HASH;
143             }
144             }
145              
146             sub _init_time_db {
147             my ($self, $lang) = @_;
148              
149             if (not exists $self->{$lang}{_time_hash}) {
150             $self->{$lang}{_time_hash} = {};
151             $self->_init_db($self->{$lang}{_time_hash}, $self->{$lang}{time_db});
152              
153             if (not exists $self->{$lang}{_time_hash}{sanitized}) {
154             $self->{$lang}{_time_hash}{sanitized} = time;
155             }
156             }
157             }
158              
159             sub _init_code_db {
160             my ($self, $lang) = @_;
161              
162             if (not exists $self->{$lang}{_code_hash}) {
163             $self->{$lang}{_code_hash} = {};
164             $self->_init_db($self->{$lang}{_code_hash}, $self->{$lang}{code_db});
165             }
166             }
167              
168             sub dbm_lookup {
169             my ($self, $lang, $md5) = @_;
170              
171             $self->_init_time_db($lang)
172             if not exists($self->{$lang}{_time_hash});
173              
174             if (exists($self->{$lang}{_time_hash}{$md5})) {
175             $self->_init_code_db($lang)
176             if not exists($self->{$lang}{_code_hash});
177              
178             if (time - $self->{$lang}{_time_hash}{$md5} >= UPDATE_SEC) {
179             $self->{$lang}{_time_hash}{$md5} = time;
180             }
181              
182             my $compressed_code = $self->{$lang}{_code_hash}{$md5};
183              
184             state $_x = require IO::Uncompress::RawInflate;
185             IO::Uncompress::RawInflate::rawinflate(\$compressed_code => \my $decompressed_code)
186             or die "rawinflate failed: $IO::Uncompress::RawInflate::RawInflateError";
187              
188             return Encode::decode_utf8($decompressed_code);
189             }
190              
191             return;
192             }
193              
194             sub dbm_store {
195             my ($self, $lang, $md5, $code) = @_;
196              
197             $self->_init_code_db($lang)
198             if not exists($self->{$lang}{_code_hash});
199              
200             # Sanitize the database, by removing old entries
201             if (time - $self->{$lang}{_time_hash}{sanitized} >= SANITIZE_SEC) {
202              
203             $self->{$lang}{_time_hash}{sanitized} = time;
204              
205             my @delete_keys;
206             while (my ($key, $value) = each %{$self->{$lang}{_time_hash}}) {
207             if (time - $value >= DELETE_SEC) {
208             push @delete_keys, $key;
209             }
210             }
211              
212             if (@delete_keys) {
213             delete @{$self->{$lang}{_time_hash}}{@delete_keys};
214             delete @{$self->{$lang}{_code_hash}}{@delete_keys};
215             }
216             }
217              
218             state $_x = require IO::Compress::RawDeflate;
219             IO::Compress::RawDeflate::rawdeflate(\$code => \my $compressed_code)
220             or die "rawdeflate failed: $IO::Compress::RawDeflate::RawDeflateError";
221              
222             $self->{$lang}{_time_hash}{$md5} = time;
223             $self->{$lang}{_code_hash}{$md5} = $compressed_code;
224             }
225              
226             sub compile_code {
227             my ($self, $code, $lang) = @_;
228              
229             $lang //= 'Sidef';
230              
231             if (
232             $self->{opt}{s}
233             ##and length($$code) > 1024
234             and (defined($self->{dbm_driver})
235             or $self->has_dbm_driver)
236             ) {
237              
238             my $db_dir = ($self->{$lang}{db_dir} //= File::Spec->catdir($self->get_sidef_vdir(), $lang));
239              
240             if (not -e $db_dir) {
241             require File::Path;
242             File::Path::make_path($db_dir);
243             }
244              
245             state $_x = do {
246             require Encode;
247             require Digest::MD5;
248             };
249              
250             my $md5 = Digest::MD5::md5_hex(Encode::encode_utf8($code));
251              
252             $self->{$lang}{time_db} //= File::Spec->catfile($db_dir, 'Sidef_Time_' . $self->{dbm_driver} . '.db');
253             $self->{$lang}{code_db} //= File::Spec->catfile($db_dir, 'Sidef_Code_' . $self->{dbm_driver} . '.db');
254              
255             if (defined(my $cached_code = $self->dbm_lookup($lang, $md5))) {
256             return $cached_code;
257             }
258              
259             my $evals_num = keys(%EVALS);
260              
261             local $self->{environment_name} = 'Sidef::Runtime' . $md5;
262             my $deparsed = $self->compile_ast($self->parse_code($code), $lang);
263              
264             if ($lang eq 'Perl') {
265             $deparsed = "package $self->{environment_name} {$deparsed}\n";
266             }
267              
268             # Don't store code that contains eval()
269             if (keys(%EVALS) == $evals_num) {
270             $self->dbm_store($lang, $md5, Encode::encode_utf8($deparsed));
271             }
272              
273             return $deparsed;
274             }
275              
276             state $count = 0;
277             local $self->{environment_name} = 'Sidef::Runtime' . (CORE::abs($count++) || '');
278              
279             my $deparsed = $self->compile_ast($self->parse_code($code), $lang);
280              
281             if ($lang eq 'Perl') {
282             $deparsed = "package $self->{environment_name} {$deparsed}\n";
283             }
284              
285             return $deparsed;
286             }
287              
288             sub compile_ast {
289             my ($self, $ast, $lang) = @_;
290              
291             $lang //= 'Sidef';
292              
293             my $module = "Sidef::Deparse::$lang";
294             my $pm = ($module =~ s{::}{/}gr . '.pm');
295              
296             require $pm;
297             $self->{$lang}{deparser} = $module->new(
298             opt => $self->{opt},
299             namespaces => $self->{namespaces},
300             environment_name => $self->{environment_name} // '',
301             );
302              
303             scalar $self->{$lang}{deparser}->deparse($ast);
304             }
305              
306             #
307             ## Util functions
308             #
309              
310             sub normalize_type {
311             my ($type) = @_;
312              
313             if (index($type, 'Sidef::') == 0) {
314             $type = substr($type, rindex($type, '::') + 2);
315             }
316             else {
317             $type =~ s/^(?:_::)?main:://
318             or $type =~ s/^_:://;
319             }
320              
321             $type =~ s/[0-9]{8}\z//r;
322             }
323              
324             sub normalize_method {
325             my ($type, $method) = ($_[0] =~ /^(.*[^:])::(.*)$/);
326             normalize_type($type) . ".$method";
327             }
328              
329             sub jaro {
330             my ($s, $t) = @_;
331              
332             my $s_len = length($s);
333             my $t_len = length($t);
334              
335             my $match_distance = int(List::Util::max($s_len, $t_len) / 2) - 1;
336              
337             my @s_matches;
338             my @t_matches;
339              
340             my @s = split(//, $s);
341             my @t = split(//, $t);
342              
343             my $matches = 0;
344             foreach my $i (0 .. $s_len - 1) {
345              
346             my $start = List::Util::max(0, $i - $match_distance);
347             my $end = List::Util::min($i + $match_distance + 1, $t_len);
348              
349             foreach my $j ($start .. $end - 1) {
350             $t_matches[$j] and next;
351             $s[$i] eq $t[$j] or next;
352             $s_matches[$i] = 1;
353             $t_matches[$j] = 1;
354             $matches++;
355             last;
356             }
357             }
358              
359             return 0 if $matches == 0;
360              
361             my $k = 0;
362             my $trans = 0;
363              
364             foreach my $i (0 .. $s_len - 1) {
365             $s_matches[$i] or next;
366             until ($t_matches[$k]) { ++$k }
367             $s[$i] eq $t[$k] or ++$trans;
368             ++$k;
369             }
370              
371             #<<<
372             (($matches / $s_len) + ($matches / $t_len)
373             + (($matches - $trans / 2) / $matches)) / 3;
374             #>>>
375             }
376              
377             sub jaro_winkler {
378             my ($s, $t) = @_;
379              
380             my $distance = jaro($s, $t);
381              
382             my $prefix = 0;
383             foreach my $i (0 .. List::Util::min(3, length($s), length($t))) {
384             substr($s, $i, 1) eq substr($t, $i, 1) ? ++$prefix : last;
385             }
386              
387             $distance + $prefix * 0.1 * (1 - $distance);
388             }
389              
390             sub best_matches {
391             my ($name, $set) = @_;
392              
393             my $max = 0;
394             my @best;
395             foreach my $elem (@$set) {
396             my $dist = sprintf("%.4f", jaro_winkler($elem, $name));
397             $dist >= 0.8 or next;
398             if ($dist > $max) {
399             $max = $dist;
400             @best = ();
401             }
402             push(@best, $elem) if $dist == $max;
403             }
404              
405             @best;
406             }
407              
408             };
409              
410             #
411             ## UNIVERSAL methods
412             #
413              
414             *UNIVERSAL::get_value = sub {
415             ref($_[0]) eq 'Sidef::Module::OO' || ref($_[0]) eq 'Sidef::Module::Func'
416             ? $_[0]->{module}
417             : $_[0];
418             };
419              
420             *UNIVERSAL::DESTROY = sub { };
421              
422             *UNIVERSAL::AUTOLOAD = sub {
423             my ($self, @args) = @_;
424              
425             $self = ref($self) if ref($self);
426              
427             if (index($self, 'Sidef::') == 0 and index($self, 'Sidef::Runtime') != 0) {
428              
429             eval { require $self =~ s{::}{/}rg . '.pm' };
430              
431             if ($@) {
432             if (defined(&main::__load_sidef_module__)) {
433             main::__load_sidef_module__($self);
434             }
435             else {
436             die "[AUTOLOAD] $@";
437             }
438             }
439              
440             if (defined(&$AUTOLOAD)) {
441             goto &$AUTOLOAD;
442             }
443             }
444              
445             my @caller = caller(1);
446             my $from = Sidef::normalize_method($caller[3]);
447             $from = $from eq '.' ? 'main()' : "$from()";
448              
449             my $table = \%{$self . '::'};
450             my @methods = grep { !ref($table->{$_}) and defined(&{$table->{$_}}) } keys(%$table);
451              
452             my $method = Sidef::normalize_method($AUTOLOAD);
453             my $name = substr($method, rindex($method, '.') + 1);
454              
455             my @candidates = Sidef::best_matches($name, \@methods);
456              
457             die( "[AUTOLOAD] Undefined method `"
458             . $method . q{'}
459             . " called from $from\n"
460             . (@candidates ? ("[?] Did you mean: " . join("\n" . (' ' x 18), sort @candidates) . "\n") : ''));
461             return;
462             };
463              
464             1;