c3-mro.pl 8.6 KB

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