c3-mro.pl 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. #!/usr/bin/env perl
  2. # License: AGL, see LICENSE file for copyright and license details.
  3. #
  4. # Abstract: Check if c3 algorithm is well understood. To do that we
  5. # define the general structure of a small DSL for defining classes
  6. # with their methods and inheritance relationships. This program
  7. # parses the DSL following __DATA__ to perform c3 linearization and
  8. # method call tests.
  9. #
  10. # CLASS_NAME: METHOD_NAME
  11. # SUBCLASS_NAME -> ( SUPERCLASS_NAME ... ) : METHOD_NAME[@] ...
  12. #
  13. # Example:
  14. # O: meth1 meth2 meth3
  15. # A -> (O) : meth2
  16. # B -> (O) : meth1
  17. # C -> (A B) : meth2
  18. #
  19. # The above example defines a section, sections are separated by
  20. # the token '###'. Sections are individually tested.
  21. #
  22. # The optional '@' following a method name implies that in its code
  23. # the method unconditionally calls its super method.
  24. #
  25. # c3 linearization and method call tests are done with the help of
  26. # Perl's c3 mro module. A Perl code is generated for each section.
  27. # Each generated Perl code defines classes from its corresponding
  28. # section and serves as a base to perform method call and c3
  29. # linearization tests.
  30. use strict;
  31. use warnings;
  32. use feature qw(current_sub postderef_qq);
  33. use List::Util qw(uniq);
  34. use Test::More;
  35. # Parse a section and return the result
  36. sub parse_section {
  37. my ($section_str, $n) = @_;
  38. my $section;
  39. while ($section_str =~
  40. /\G ([A-Z]) (?: \h* -> \h* \( (?<s>[A-Z](?:\h++[A-Z])*) \) )? (?: \h* : \h* (?<m>\w+[@]?(?:\h++\w++)*) )? \s* /gx) {
  41. die "[section $n]: already defined class '$1'\n" if exists $section->{$1};
  42. $section->{$1} = {};
  43. $section->{$1}{m} = [split " ", $+{m}] if defined $+{m};
  44. next unless exists $+{s};
  45. my %def;
  46. my @supers = split " ", $+{s};
  47. foreach my $super (@supers) {
  48. die "[section $n]: class '$1' cannot inherit himself\n" if $1 eq $super;
  49. die "[section $n]: class '$super' is undefined\n" unless exists $section->{$super};
  50. die "[section $n]: duplicate class '$super'\n" if exists $def{$super};
  51. $def{$super} = 1;
  52. push $section->{$1}{s}->@*, $super;
  53. }
  54. }
  55. return $section;
  56. }
  57. # Parse each section and return the result
  58. sub parse_sections {
  59. my $data = shift;
  60. my ($n, $sections) = (0);
  61. push @$sections, parse_section($1, $n++) while $data =~ /\G\s*(.+?)(?:###|\z)/gs;
  62. return $sections;
  63. }
  64. # Generate and return Perl code of a section
  65. sub gen_perl_code {
  66. my $section = shift;
  67. my $perl_code = <<~'EOF';
  68. use strict;
  69. use warnings;
  70. use mro "c3";
  71. EOF
  72. foreach my $class (sort keys %$section) {
  73. my $class_info = $section->{$class};
  74. $perl_code .= "package $class {";
  75. if (exists $class_info->{s}) {
  76. $perl_code .= exists $class_info->{m} ? "\n\t" : ' ';
  77. $perl_code .= "use parent -norequire, qw(@{$class_info->{s}});";
  78. }
  79. if (exists $class_info->{m}) {
  80. my $len = $class_info->{m}->@*;
  81. my $sep = $len > 1 || exists $class_info->{s} ? "\n" : ' ';
  82. for (my $i = 0 ; $i < $len ; $i++) {
  83. my $method = $class_info->{m}[$i];
  84. my $name = $method =~ s/[@]$//r;
  85. my $tab = !exists $class_info->{s} && ($i - 1) < 0 && ($i + 1 == $len) ? '' : "\t";
  86. $perl_code .= qq[${sep}${tab}sub $name { print "${class}::$name"];
  87. $perl_code .= '; $_[0]->next::method()' if $method =~ /[@]$/;
  88. $perl_code .= ' }';
  89. }
  90. $perl_code .= "$sep}\n";
  91. }
  92. else {
  93. $perl_code .= " }\n";
  94. }
  95. }
  96. return $perl_code;
  97. }
  98. sub cmp_val {
  99. return lc $_[0] =~ s/["',]//gr =~ s/\s+/ /gr;
  100. }
  101. # Compute the C3 linearization of a section's class. The dynamic hash
  102. # table $c3 keeps track of all c3 results of classes defined in the
  103. # section of concern, set it to `undef' when entering a new section.
  104. # In Maat, linearization is done at compilation
  105. sub c3_linearize_class {
  106. my ($section, $class) = (shift, shift);
  107. $_[0]->{$class} = [$class];
  108. # Has no superclass? probably 'A'
  109. return unless exists $section->{$class}{s};
  110. my ($supers, $i) = ($section->{$class}{s}, -1);
  111. # Just in case subroutine `test_c3' does not linearize classes in order of their definitions
  112. while (++$i < @$supers) {
  113. __SUB__->($section, $supers->[$i], $_[0]) unless exists $_[0]->{$supers->[$i]};
  114. return unless ref $_[0];
  115. }
  116. # Single inheritance? simply push
  117. if (@$supers == 1) {
  118. push @{$_[0]->{$class}}, $_[0]->{$supers->[0]}->@*;
  119. return;
  120. }
  121. my ($prev_sol, $error);
  122. my $sub_sol = $_[0]->{$supers->[0]};
  123. my %merged = ($supers->[0] => 0);
  124. foreach my $c (1 .. $#$supers) {
  125. my $start = 0;
  126. my $to_insert = $_[0]->{$supers->[$c]};
  127. $prev_sol = $sub_sol;
  128. $sub_sol = [];
  129. insertion: for (my $i = 0 ; $i < @$prev_sol ; $i++) {
  130. for (my $j = $start ; $j < @$to_insert ; $j++) {
  131. if (exists $merged{$to_insert->[$j]}) {
  132. $_[0] = <<~"EOE";
  133. Inconsistent hierarchy during C3 merge of class $class:
  134. current merge results
  135. [ $class, @$supers[0 .. $merged{$to_insert->[$j]} - 1] ]
  136. merging failed on $to_insert->[$j] at
  137. EOE
  138. return;
  139. }
  140. if ($prev_sol->[$i] eq $to_insert->[$j]) {
  141. push @$sub_sol, $to_insert->@[$start .. $j];
  142. $start = $j + 1;
  143. next insertion;
  144. }
  145. }
  146. push @$sub_sol, $prev_sol->[$i];
  147. }
  148. $merged{$supers->[$c]} = $c;
  149. }
  150. push @{$_[0]->{$class}}, @$sub_sol;
  151. }
  152. # For each class in a section, resolve all its methods, including
  153. # the inherited ones is done at class creation, this makes vm
  154. # execution fast. This is done by walking down the C3 list containing
  155. # the directly or indirectly inherited classes to bind their methods
  156. # to the base class so that the base class has direct access to
  157. # them during method calls.
  158. sub resolve_methods {
  159. my ($section, $c3s) = @_;
  160. my $resolved_meths;
  161. foreach my $class (sort keys %$section) {
  162. my $class_c3 = $c3s->{$class};
  163. foreach my $sup (@$class_c3) {
  164. push $resolved_meths->{$class}{$_ =~ s/[@]$//r}->@*, "${sup}::$_" foreach $section->{$sup}{m}->@*;
  165. }
  166. }
  167. return $resolved_meths;
  168. }
  169. # Simulate the $method method call on object $object
  170. sub call_method {
  171. my ($resolved_meths, $object, $method) = @_;
  172. if (exists $resolved_meths->{$object}{$method}) {
  173. my $output;
  174. my $meth_list = $resolved_meths->{$object}{$method};
  175. for (my $i = 0 ; $i < @$meth_list ; $i++) {
  176. $output .= $meth_list->[$i] =~ s/[@]$//r;
  177. if ($meth_list->[$i] =~ /[@]$/) {
  178. $output .= "No next::method $method found for $object at" if $i == $#$meth_list;
  179. next;
  180. }
  181. return $output;
  182. }
  183. }
  184. return "Can't locate object method $method via package $object at";
  185. }
  186. sub perl_c3_linearize_class {
  187. my $class = shift;
  188. return $_[0] . <<~"EOP";
  189. print qq[\@{mro::get_linear_isa("$class", "c3")}];
  190. EOP
  191. }
  192. sub perl_call_method {
  193. my ($class, $method) = (shift, shift);
  194. return $_[0] . "\n$class->$method();";
  195. }
  196. # Run generated perl code and return its stdout+stderr
  197. sub perl_run {
  198. my $code = shift;
  199. local $/;
  200. open(my $fh, '-|', "perl -E '$code' 2>&1") or die "$!\n";
  201. return <$fh>;
  202. }
  203. sub test_call {
  204. my ($resolved_meths, $class, $meth, $code, $i) = @_;
  205. my $expected = cmp_val(perl_run(perl_call_method($class, $meth, $code)));
  206. my $result = cmp_val(call_method($resolved_meths, $class, $meth));
  207. ok($expected =~ qr/^\Q$result\E/, "[section $i] is '$class'->$meth() okay given [$resolved_meths->{$class}{$meth}->@*]?");
  208. }
  209. # This is the entry point our program. It tests with the help of Perl's C3
  210. # our own c3 implementation, It compares Perl's linearization of a class
  211. # with our own c3 list of the class, it performs series of method calls
  212. # to check.
  213. sub test_c3 {
  214. local $/;
  215. my $sections = parse_sections(<DATA>);
  216. my @codes = map gen_perl_code($_), @$sections;
  217. my $n_tests = 0;
  218. local $" = ', ';
  219. section: for (my $i = 0 ; $i < @$sections ; $i++) {
  220. my $c3s;
  221. my $section = $sections->[$i];
  222. # Just like real compilers do, linearize classes in order of their definitions
  223. foreach my $class (sort keys %$section) {
  224. my $expected = perl_run(perl_c3_linearize_class($class, $codes[$i]));
  225. $n_tests++;
  226. c3_linearize_class($section, $class, $c3s);
  227. if (ref $c3s) {
  228. my $c3_result = join ' ', $c3s->{$class}->@*;
  229. is($c3_result, $expected, "[section $i]: is c3 of '$class' [$c3s->{$class}->@*]?");
  230. }
  231. else {
  232. ($c3s, $expected) = (cmp_val($c3s), cmp_val($expected));
  233. ok($expected =~ qr/^\Q$c3s\E/, "[section $i]: failed to find c3 of '$class'?");
  234. next section;
  235. }
  236. }
  237. my $resolved_meths = resolve_methods($section, $c3s);
  238. # Test if mro is okay
  239. foreach my $class (sort keys %$section) {
  240. test_call($resolved_meths, $class, $_, $codes[$i], $i), $n_tests++ foreach keys $resolved_meths->{$class}->%*;
  241. # Call methods that were not inherited by $class to see they are actually failing
  242. test_call($resolved_meths, $class, $_, $codes[$i], $i), $n_tests++
  243. foreach grep { !exists $resolved_meths->{$class}{$_} }
  244. uniq map { s/[@]?$//r } map { $section->{$_}{m}->@* } keys %$section;
  245. }
  246. }
  247. return $n_tests;
  248. }
  249. done_testing(test_c3());
  250. __DATA__
  251. A
  252. B -> (A)
  253. C -> (A)
  254. D -> (C A B)
  255. ###
  256. A
  257. ###
  258. A
  259. B -> (A)
  260. C -> (A)
  261. D -> (A B C)
  262. ###
  263. A : meth1 meth2 meth3
  264. B -> (A) : meth2
  265. C -> (A) : meth1@
  266. D -> (A) : meth2
  267. E -> (B)
  268. F -> (C)
  269. G -> (C)
  270. H -> (C)
  271. I -> (C)
  272. J -> (D)
  273. K -> (E)
  274. L -> (F I)
  275. M -> (I J)
  276. N -> (K E L M)