🔬61. Declared in BOOTSTRAP

First of all, a new release of the Rakudo Perl 6 compiler was announced today: 2018.02. There are many fixes and speed improvements there, including one proposed by me. Let me not go through the changes, as most of them require quite in-depth knowledge of the Rakudo internals.

Instead, let us take a low-hanging fruit and look at the feature that you may see almost immediately when you start reading Rakudo sources.

Ideologically, Perl 6 can (and should) be written in Perl 6. Currently, some parts are written in NQP but still, the vast number of data types—located in the src/core directory—are implemented in Perl 6.

The thing is that some classes are not fully defined there. Or their relation to other classes is not explicit. For example, here’s the whole definition of the Sub class:

my class Sub { # declared in BOOTSTRAP
    # class Sub is Routine


Not only you don’t see any methods here, but also its hierarchy is defined ‘via comments.’ Of course, Perl 6 is not that smart to read comments saying ‘make this code great and cool,’ so let’s see what’s going on here.

In the source tree, there is the following file: src/Perl6/Metamodel/BOOTSTRAP.nqp, where the above-mentioned relation is built.

The class itself (the Sub name) is declared as a so-called stub in the very beginning of the file:

my stub Sub metaclass Perl6::Metamodel::ClassHOW { ... };

Now, the name is known but the definition is not yet ready. We have seen a few examples earlier. Here is the part of the Sub class:

# class Sub is Routine {
Sub.HOW.add_parent(Sub, Routine);

This code lets the user think that the class definition is the following, as the documentation says:

class Sub is Routine {

Other examples of Routine children are Method, Submethod, and Macro. The first two are also defined in BOOTSTRAP:

# class Method is Routine {
Method.HOW.add_parent(Method, Routine);

# class Submethod is Routine {
Submethod.HOW.add_parent(Submethod, Routine);

The classes themselves are defined in their corresponding files src/core/Method.pm and src/core/Submethod.pm:

my class Method { # declared in BOOTSTRAP
    # class Method is Routine

    multi method gist(Method:D:) { self.name }
my class Submethod { # declared in BOOTSTRAP
    # class Submethod is Routine

    multi method gist(Submethod:D:) { self.name }

Unlike them, the Marco type’s hierarchy is explicitly announced in src/core/Macro.pm:

my class Macro is Routine {

As you may see, the classes basically introduce their namespaces and do not add many methods to their Routine parent.

The Routine class in its turn is also defined in two places: in src/core/Routine.pm and in BOOTSTRAP.pm.

my class Routine { # declared in BOOTSTRAP
    # class Routine is Block
    # has @!dispatchees;
    # has Mu $!dispatcher_cache;
    # has Mu $!dispatcher;
    # has int $!rw;
    # has Mu $!inline_info;
    # has int $!yada;
    # has Mu $!package;
    # has int $!onlystar;
    # has @!dispatch_order;
    # has Mu $!dispatch_cache;

This time, there are many methods, some of which are added in src/core/Routine.pm using regular Perl 6 syntax, and some are added through BOOTSTRAP in NQP:

In Perl 6:

method candidates() {
    self.is_dispatcher ??
        nqp::hllize(@!dispatchees) !!


Routine.HOW.add_method(Routine, 'dispatcher', nqp::getstaticcode(sub ($self) {
        Routine, '$!dispatcher')

Similarly, the attributes from comments are created in NQP:

Routine.HOW.add_attribute(Routine, Attribute.new(:name<@!dispatchees>, :type(List), :package(Routine)));
Routine.HOW.add_attribute(Routine, Attribute.new(:name<$!dispatcher_cache>, :type(Mu), :package(Routine)));

As far as I understand, such bootstrapping is needed because Rakudo requires some Perl 6 defined before it can compile itself. For example, if you declare Sub’s relation to Routine completely in src/core/Sub.pm, then you get an error when compiling Rakudo:

Representation for Sub must be composed before it can be serialized

🔬60. Examining the Real role of Perl 6, part 3

As promised yesterday, let us take a look at the two methods of the Real role: polymod and base.


I already devoted a post to the Int.polymod method, but the method also exists in the Real role. Let us see if it is different.

method polymod(Real:D: +@mods) {
    my $more = self;
    my $lazy = @mods.is-lazy;
    fail X::OutOfRange.new(
        :what('invocant to polymod'), :got($more), :range<0..Inf>
    ) if $more < 0; 
    gather { 
        for @mods -> $mod {
            last if $lazy and not $more;
                using => 'polymod', numerator => $more
            ) unless $mod;
            take my $rem = $more % $mod;
            $more -= $rem;
            $more /= $mod;
        take $more if ($lazy and $more) or not $lazy;

It looks familiar. Comparing to the method of Int, the separation of lazy and non-lazy lists is incorporated in the main loop. In the rest, it is again the mod operation (in the form of %) and a division (and some additional subtraction).

Try the method on the same 120 (but as a Numeric value):

> say 120.polymod(10,10)
(0 2 1)

> say 120e0.polymod(10,10)
(0 2 1)

The first method is a call of Int.polymod, while the second one is Real.polymod. The results are the same.

A final note on the method. Just notice that it also works with non-integer values:

> 120.34.polymod(3.3, 4.4)
(1.54 0.8 8)

Indeed, 1.54 + 0.8 * 3.3 + 8 * 3.3 * 4.4 = 120.34.


The base method converts a number to its representation in a different system, e. g., hexadecimal, octal, or in a system with 5 or 35 digits. Extrapolating hexadecimal system, you may guess that if there are 36 digits, then the digits are 0 to 9 and A to Z.

A few examples with the numbers with a floating point (actually, Rat numbers here):

> 120.34.base(10)
> 120.34.base(36)
> 120.34.base(3)
> 120.34.base(5)

The fractional part is converted separately. The second argument of the method limits the number of digits in it. Compare:

> 120.34.base(5)
> 120.34.base(5, 2)

I will skip the details of the method internals and will only show the most interesting parts.

The signature of the method in the src/core/Real.pm file is the following:

 method base(Int:D $base, $digits? is copy)

The documentation interprets that quite differently (although correct semantically):

method base(Real:D: Int:D $base where 2..36, $digits? --> Str:D)

The possible digits are listed explicitly (not in ranges):

my @conversion := <0 1 2 3 4 5 6 7 8 9
                   A B C D E F G H I J
                   K L M N O P Q R S T
                   U V W X Y Z>;

Finally, the last gathering of the separate digits into a resulting string is done like that, using a call to the Int.base method:

my Str $r = $int_part.base($base);
$r ~= '.' ~ @conversion[@frac_digits].join if @frac_digits;
# if $int_part is 0, $int_part.base doesn't see the sign of self
$int_part == 0 && self < 0 ?? '-' ~ $r !! $r;

The method also does some heuristics to determine the number of digits after the floating point:

my $prec = $digits // 1e8.log($base.Num).Int;
. . .
for ^$prec {
    last unless $digits // $frac;
    $frac = $frac * $base;
    push @frac_digits, $frac.Int;
    $frac = $frac - $frac.Int;

Compare now the method with the same method from the Int class:

multi method base(Int:D: Int:D $base) {
    2 <= $base <= 36
        ?? nqp::p6box_s(nqp::base_I(self,nqp::unbox_i($base)))
        !! Failure.new(X::OutOfRange.new(
            what => "base argument to base", :got($base), :range<2..36>))

In this case, all the hard work is delegated to the base_I function of NQP.

And that’s more or less all that I wanted to cover from the Real role internals.

🔬59. Examining the Real role of Perl 6, part 2

Today, we continue our initial exploration of the Real role, that was started a couple of days ago.

Together with its methods, the role contains a number of subroutines (placed outside the role) that define the infix operators with the objects of the Real type. The list is not that long, so let me copy it here:

multi sub infix:<+>(Real \a, Real \b) { a.Bridge + b.Bridge }
multi sub infix:<->(Real \a, Real \b) { a.Bridge - b.Bridge }
multi sub infix:<*>(Real \a, Real \b) { a.Bridge * b.Bridge }
multi sub infix:</>(Real \a, Real \b) { a.Bridge / b.Bridge }
multi sub infix:<%>(Real \a, Real \b) { a.Bridge % b.Bridge }
multi sub infix:<**>(Real \a, Real \b) { a.Bridge ** b.Bridge }
multi sub infix:«<=>»(Real \a, Real \b) { a.Bridge <=> b.Bridge }
multi sub infix:<==>(Real \a, Real \b) { a.Bridge == b.Bridge }
multi sub infix:«<»(Real \a, Real \b) { a.Bridge < b.Bridge }
multi sub infix:«<=»(Real \a, Real \b) { a.Bridge <= b.Bridge }
multi sub infix:«≤» (Real \a, Real \b) { a.Bridge ≤ b.Bridge }
multi sub infix:«>»(Real \a, Real \b) { a.Bridge > b.Bridge }
multi sub infix:«>=»(Real \a, Real \b) { a.Bridge >= b.Bridge }
multi sub infix:«≥» (Real \a, Real \b) { a.Bridge ≥ b.Bridge }

proto sub infix:<mod>($, $) is pure {*}
multi sub infix:<mod>(Real $a, Real $b) {
    $a - ($a div $b) * $b;

As you see, most of the operators are using the Bridge method, which allows using the same code in derived classes that may redefine the bridge.

There’s also one prefix operation for negation:

multi sub prefix:<->(Real:D \a) { -a.Bridge }

The cis function works as a type converter returning a complex number:

roto sub cis($) {*}
multi sub cis(Real $a) { $a.cis }

Try it out:

> cis(pi)

> cis(pi).WHAT

A bit outstanding, there is a function for atan2:

proto sub atan2($, $?) {*}
multi sub atan2(Real \a, Real \b = 1e0) { a.Bridge.atan2(b.Bridge) }
# should really be (Cool, Cool), and then (Cool, Real) and (Real, Cool)
# candidates, but since Int both conforms to Cool and Real, we'd get lots
# of ambiguous dispatches. So just go with (Any, Any) for now.
multi sub atan2( \a, \b = 1e0) { a.Numeric.atan2(b.Numeric) }

It is a bit strange as it does not follow the manner in which other trigonometric functions are implemented. The atan2 routine is also defined as a method:

proto method atan2(|) {*}
multi method atan2(Real $x = 1e0) { self.Bridge.atan2($x.Bridge) }
multi method atan2(Cool $x = 1e0) { self.Bridge.atan2($x.Numeric.Bridge) }

All other trigonometric functions only exist as Real methods. The rest is defined inside the src/core/Numeric.pm file as self-standing subroutines, for example:

proto sub cos($) is pure {*}
multi sub cos(Numeric \x) { x.cos }
multi sub cos(Cool \x) { x.Numeric.cos }

. . .

proto sub atan($) is pure {*}
multi sub atan(Numeric \x) { x.atan }
multi sub atan(Cool \x) { x.Numeric.atan }

There are a few more routines but let us skip them, as they are quite straightforward and clear.

In the next part, we will explore the two methods of the Real role: polymod and base. Stay tuned!

🔬58. A word on polymod in Perl 6

Before moving to the second part of the Real role, let us stop on the polymod method of the Int class.

The method takes a number and a list of arbitrary numbers (units) and returns the corresponding multipliers. So that you can easily say that 550 seconds, for example, is 9 minutes and 10 seconds:

> 550.polymod(60)
(10 9)

In the method call, the value of 60 is the number of seconds in a minute. In the result, 9 is a number of minutes, and 10 is a remainder, which is a number of seconds. So, 550 seconds = 10 second + 9 minutes.

If you want more details, add more units. For example, what is it 32768 seconds?

> 32768.polymod(60, 60, 24)
(8 6 9 0)

It is 8 seconds, 6 minutes, 9 hours, and 0 days.

Similarly, 132768 seconds are 1 day, 12 hours, 52 minutes, and 48 seconds:

> 132768.polymod(60, 60, 24)
(48 52 12 1)

Honestly, it was quite difficult for me to understand how it works and how to read the result.

Another example from the documentation was even harder:

> 120.polymod(1, 10, 100)
(0 0 12 0)

What does 12 mean? It is, obviously, 12 times 10. OK, But I asked to give me some information about the number of hundreds. My expectation is to have it like that: 120 is 2 times 10 and 1 time 100.

Try 121:

> 121.polymod(1, 10)
(0 1 12)

Erm, why zero? Zero plus 1 times 1 plus 12 times 10? Brr. Ah! You don’t need to specify an explicit 1 in the arguments:

> 121.polymod(10)
(1 12)

That makes more sense. Except the fact that I still don’t know how many hundreds are there in 121:

> 121.polymod(10, 100)
(1 12 0)
> 121.polymod(100, 10)
(21 1 0)

It’s time to take a look at the source code (src/core/Int.pm):

method polymod(Int:D: +@mods) {
    fail X::OutOfRange.new(
        :what('invocant to polymod'), :got(self), :range<0..^Inf>
    ) if self < 0; 

    gather { 
         my $more = self; 
         if @mods.is-lazy { 
             for @mods -> $mod {
                    ?? $mod
                    ?? take $more mod $mod
                    !! Failure.new(X::Numeric::DivideByZero.new:
                            using => 'polymod', numerator => $more)
                    !! last;
                $more = $more div $mod;
            take $more if $more;
        else {
            for @mods -> $mod {
                    ?? take $more mod $mod
                    !! Failure.new(X::Numeric::DivideByZero.new:
                        using => 'polymod', numerator => $more);
                $more = $more div $mod;
            take $more;

The method has two branches, one for lazy lists, and another one for non-lazy lists. Let us only focus on the second branch for now:

for @mods -> $mod {
        ?? take $more mod $mod
        !! Failure.new(X::Numeric::DivideByZero.new:
                       using => 'polymod', numerator => $more);
    $more = $more div $mod;

take $more;

OK, the last take takes the remainder, that’s easy. In the loop, you divide the number by the next unit and then ‘count’ the intermediate reminder.

I would say I would implement it differently and switch the operators:

  for @mods -> $mod {
-           ?? take $more mod $mod
+           ?? take $more div $div
          !! Failure.new(X::Numeric::DivideByZero.new:
                         using => 'polymod', numerator => $more);
-      $more = $more div $mod;
+      $more = $more mod $mod;

  take $more;

With this code, I can get the number of hundreds, tens, and ones in 121:

> 121.polymod(100, 10, 1)
(1 2 1 0)

OK, let’s avoid two 1s:

> 1234.polymod(1000, 100, 10, 1)
(1 2 3 4 0)

Also works fine with the earlier example with seconds:

> 132768.polymod(86400, 3600, 60)
(1 12 52 48)

It is 1 day, 12 hours, 52 minutes, and 48 seconds.

As you see, now you have to use explicit units (8600 instead of 24) and you have to sort them in descending order, but now I can understand and explain the result, which I could hardly do for the original method.


🔬57. Examining the Real role of Perl 6, part 1

During the last few days, we talked a lot about the Real role. Lets us then look at it more precisely. The code is located in the src/core/Real.pm file.

It contains the role itself and a few subroutines implementing different infixes. The Real role in its turn implements the Numeric role:

my role Real does Numeric {
    . . .

It is interesting that the class definition also needs some knowledge about the Complex class, that’s why there is a forward class declaration in the first line of the file:

my class Complex { ... }

The Real role defines many trigonometrical functions as methods, and as we already saw, they are using the Bridge method:

method sqrt() { self.Bridge.sqrt }
method rand() { self.Bridge.rand }
method sin() { self.Bridge.sin }
method asin() { self.Bridge.asin }
method cos() { self.Bridge.cos }
method acos() { self.Bridge.acos }
method tan() { self.Bridge.tan }
method atan() { self.Bridge.atan }

Another set of methods include generic methods that manipulate the value directly:

method abs() { self < 0 ?? -self !! self }
proto method round(|) {*}
multi method round(Real:D:) {
    (self + 1/2).floor; # Rat NYI here, so no .5
multi method round(Real:D: Real() $scale) {
    (self / $scale + 1/2).floor * $scale;
method truncate(Real:D:) {
    self == 0 ?? 0 !! self < 0 ?? self.ceiling !! self.floor

There’s a really interesting and useful variant of the round method, which allows you to align the number to the grid you need:

> 11.5.round(3)
> 10.1.round(3)

Another set of methods are used to convert a number to different data types:

method Rat(Real:D: Real $epsilon = 1.0e-6) { self.Bridge.Rat($epsilon) }
method Complex() { Complex.new(self.Num, 0e0) }
multi method Real(Real:D:) { self }
multi method Real(Real:U:) {
    self.Mu::Real; # issue a warning;
method Bridge(Real:D:) { self.Num }
method Int(Real:D:) { self.Bridge.Int }
method Num(Real:D:) { self.Bridge.Num }
multi method Str(Real:D:) { self.Bridge.Str }

And here we have a problem in the matrix. The Bridge method is defined in such a way that it calls the Num method. In its turn, Num is calling Bridge, which calls Num.

Run one of the following lines of code, and Rakudo will hang:



🔬56. A bit more on Rat vs FatRat in Perl 6

Yesterday, we were digging into Rakudo Perl 6 to understand when a Rat value becomes a Num value. It turned out that if the value becomes too small, which means its denominator gets bigger and bigger, Rakudo starts using a Num value instead of Rat.

We found the place where it happened. Today, let us make an exercise and see if it is possible that Perl 6 behaves differently, namely, it expands the data type instead of switching it to a floating point and losing accuracy.

The change is simple. All you need is to update the ifs inside the DIVIDE_N routine:

--- a/src/core/Rat.pm
+++ b/src/core/Rat.pm
@@ -48,16 +48,14 @@ sub DIVIDE_NUMBERS(Int:D \nu, Int:D \de, \t1, \t2) {
           ($numerator   := -$numerator),
           ($denominator := -$denominator))),
-        nqp::istype(t1, FatRat) || nqp::istype(t2, FatRat),
+        nqp::istype(t1, FatRat) || nqp::istype(t2, FatRat) || $denominator >= UINT64_UPPER,
-        nqp::if(
-          $denominator < UINT64_UPPER,
-            Rat,'$!denominator',$denominator),
-          nqp::p6box_n(nqp::div_In($numerator, $denominator)))))
+            Rat,'$!denominator',$denominator)
+        ))

Now, there are two outcomes: either the routine generates a Rat value or a FatRat. The latter happens when the sub arguments were already FatRats or when the current Rat gets too close to zero.

Compile and test our modified perl6 executable with Newton’s algorithm from yesterday’s post:

my $N = 25;
my @x = 
    Rat.new(1, 1), 
    -> $x { 
        $x - ($x ** 2 - $N) / (2 * $x)
    } ... *;

.WHAT.say for @x[0..10];
.say for @x[1..10];

As expected, the first elements of the sequence are Rats, while the tail is made of FatRats:


Also, you can easily see it if you print the values:


* * *

I don’t know what is better — to have two different types for a rational number (not counting the Rational role) or one type that can hold both ‘narrow’ and ‘wide’ values, or a mechanism that switches to a wider data type when there is not enough capacity. I feel the best is the last option (in the case that FatRat and Rat are using different types for storing numerators and denominators, of course).

As far as I understand, that was exactly the original thought:

For values that do not already do the Numeric role, the narrowest appropriate type of IntRatNum, or Complex will be returned; however, string containing two integers separated by a /will be returned as a Rat (or a FatRat if the denominator overflows an int64).

Also it feels more natural to silently add more space for more digits instead of breaking the idea of having the Rat type. Anyway, there are different opinions on this, but that should not stop Perl 6 from being widespread.

🔬55. FatRat vs Rat in Perl 6

Yesterday, Solomon Foster posted an example in the Perl 6 group on Facebook:

my @x = 
    FatRat.new(1, 1), 
    -> $x { $x - ($x ** 2 - $N) / (2 * $x) } ... *

This code implements Newton’s method of finding an approximate value of a square root of $N. The important thing is that it is using a FatRat value for higher accuracy.

Let us run it for the value of 9:

my $N = 9;

my @x = 
    Rat.new(1, 1), 
    -> $x { $x - ($x ** 2 - $N) / (2 * $x) } ... *;

.say for @x[0..7];

Very soon, it converges to the correct value:


If you narrow the data type down to Rat, then, after a certain point, it will not be wide enough to keep all the decimal digits:


Of course, the absolutely correct result is achieved faster but we understand that that is due to the lack of Rat’s capacity in comparison to FatRat, and it won’t work for non-integer results, probably. Let us try it with $N = 5 and with ten iterations:

With Rat:


With FatRat:


(I hope your browser just scrolls the code block instead of wrapping the text to the new lines.)

OK, we see that FatRat gives more digits. Now look at the definition of the classes in src/core/Rat.pm:

# XXX: should be Rational[Int, uint]
my class Rat is Cool does Rational[Int, Int] { . . . }

my class FatRat is Cool does Rational[Int, Int] { . . . }

Both classes implement a parameterised Rational role. In both cases, the parameters are the same — both the numerator and the denominator are supposed to be Int values. So, why do the results of the Newton’s approximation were so different if we use the same data type to represent the fraction? Let us try figuring that out.

Just to see how the parts of the fraction are constructed in the role, here’re their declarations:

my role Rational[::NuT = Int, ::DeT = ::("NuT")] does Real {
    has NuT $.numerator = 0;
    has DeT $.denominator = 1;

So, in both cases, we have the following attributes:

has Int $.numerator = 0;
has Int $.denominator = 1;

Let’s check if the types are not changed somewhere inside the black box:

my $N = 5;
my @x = FatRat.new(1, 1), -> $x { $x - ($x ** 2 - $N) / (2 * $x) } ... *;

my $v = @x[7];
say $v.numerator.WHAT;
say $v.denominator.WHAT;

With the FatRat type, the program confirms Ints:

$ perl6 sqr-fatrat.pl 

Now try the Rat type:

$ perl6 sqr-fatrat.pl 
No such method 'numerator' for invocant of type 'Num'.
Did you mean 'iterator'?
  in block <unit> at sqr-fatrat.pl line 10

What?! What do you mean? Does it mean that the values are not Rats anymore? Let’s check it and print the types of the data elements. We know that we started the sequence with a Rat value:

Rat.new(1, 1), -> $x { $x - ($x ** 2 - $N) / (2 * $x) } ... *;

The pointy block is a generator of the next elements, so nothing bad here. Nevertheless, let’s call .WHAT on each element:

.WHAT.say for @x[0..10];

Here is the output:


The first seven items were Rats, while the rest became Nums! That explains why we saw different results, of course. But why was the data type changed?

Try it slowly. First, see the type of the value immediately after it is generated:

my $N = 5;
my @x = 
    Rat.new(1, 1), 
    -> $x {
        my $n = $x - ($x ** 2 - $N) / (2 * $x);
        say $n.WHAT;
    } ... *;

It prints the same list of types as before.

The second step is to try to force the data type everywhere we can:

my Rat $N = Rat.new(5);
my Rat @x = 
    Rat.new(1, 1), 
    -> Rat $x {
        my Rat $n = $x - ($x ** 2 - $N) / (2 * $x);
        say $n.WHAT;
 } ... *;

Run the program and see what it says:

Type check failed in assignment to $n;
expected Rat but got Num (2.23606797749979e0)
   in block <unit> at sqr-fatrat.pl line 5

It became even worse 🙂 If you force FatRat, then everything stays fine. OK, now it is really time to understand when the border is crossed and the data type is changed.

Let us visualise it a bit more so that we see the numerator and the denominator of the generated number, as well as the type of the intermediate value that is used to calculate it:

-> Rat $x {
    my $d = ($x ** 2 - $N);
    say $d;
    say $d.WHAT;
    my Rat $n = $x - $d / (2 * $x);
    say $n.WHAT;
    say $n.nude;
} ... *;

This gives us the way to confirm that it breaks:

(3 1)
(7 3)
(47 21)
(2207 987)
(4870847 2178309)
(23725150497407 10610209857723)
Type check failed in assignment to $n; expected Rat but got Num (2.23606797749979e0)
 in block <unit> at /Users/ash/Books/perl6.ru/sqr-fatrat.pl line 9

As you see, just before the exception, the parts of the fraction became quite big, and the type of the subexpression turned from Rat to Num. At that moment, the value of $d gets close to zero (this value is the error between the correct answer and the approximated value on the previous iteration).

Seems like we are close to the final destination. Once again, look at the changes of the $d variable:

say $d.perl ~ ' = ' ~ $d;

During the first few iterations, it goes to exact zero:

-4.0 = -4
4.0 = 4
<4/9> = 0.444444
<4/441> = 0.009070
<4/974169> = 0.0000041
<4/4745030099481> = 0.00000000000084
0e0 = 0

This is not happening when FatRat values are used:

FatRat.new(-4, 1) = -4
FatRat.new(4, 1) = 4
FatRat.new(4, 9) = 0.444444
FatRat.new(4, 441) = 0.009070
FatRat.new(4, 974169) = 0.0000041
FatRat.new(4, 4745030099481) = 0.00000000000084
FatRat.new(4, 112576553224922323902744729) = 0.0000000000000000000000000355
. . .

We now get to the ** operation for Rats (remember that $d = ($x ** 2 - $N)?):

multi sub infix:<**>(Rational:D \a, Int:D \b) {
    b >= 0
           (a.numerator ** b // fail (a.numerator.abs > a.denominator ?? X::Numeric::Overflow !! X::Numeric::Underflow).new),
           a.denominator ** b, # we presume it likely already blew up on the numerator
           (a.denominator ** -b // fail (a.numerator.abs < a.denominator ?? X::Numeric::Overflow !! X::Numeric::Underflow).new),
           a.numerator ** -b,

In our case, b is always non-negative, and we go to our old friend, DIVIDE_NUMBERS.

sub DIVIDE_NUMBERS(Int:D \nu, Int:D \de, \t1, \t2) {
    . . .
        $denominator < UINT64_UPPER,
        nqp::p6box_n(nqp::div_In($numerator, $denominator)))))

Yes! Finally, you can see that if the denominator is not big enough, a Rat number is returned. Otherwise, the nqp::p6box_n function creates a Num value. For a FatRat, there is a different branch that does not do the check:

nqp::istype(t1, FatRat) || nqp::istype(t2, FatRat),

The function is a Perl 6 extension to NQP, there’s a documentation line in docs/ops.markdown:

## p6box_n
* p6box_n(num $value)
Box a native num into a Perl 6 Num.

Congratulations, we’ve tracked that down!