Sidef.pm 13 KB

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