🔬14. Tracking down the ‘say’ calls in Perl 6

Welcome back! Today, we’ll try to do a simple thing using some knowledge from the previous days.

Compare the two lines:

say 'Hello, World';
'Hello, World'.say;

Is there any difference between them? Well, of course. Although the result is the same in both cases, syntactically they differ a lot.

In the first case, say is a stand-alone function that gets a string argument. In the second case, the say method is called on a string.

Compare the two lines on the parse level. First, as a function call:

- statementlist: say 'Hello, World'
  - statement: 1 matches
    - EXPR: say 'Hello, World'
      - args:  'Hello, World'
        - arglist: 'Hello, World'
          - EXPR: 'Hello, World'
            - value: 'Hello, World'
              - quote: 'Hello, World'
                - nibble: Hello, World
      - longname: say
        - name: say
          - identifier: say
          - morename:  isa NQPArray
        - colonpair:  isa NQPArray

Second, as a method:

- statementlist: 'Hello, World'.say
  - statement: 1 matches
    - EXPR: .say
      - 0: 'Hello, World'
        - value: 'Hello, World'
          - quote: 'Hello, World'
            - nibble: Hello, World
      - dotty: .say
        - sym: .
        - dottyop: say
          - methodop: say
            - longname: say
              - name: say
                - identifier: say
                - morename:  isa NQPArray
              - colonpair:  isa NQPArray
        - O: 
      - postfix_prefix_meta_operator:  isa NQPArray
      - OPER: .say
        - sym: .
        - dottyop: say
          - methodop: say
            - longname: say
              - name: say
                - identifier: say
                - morename:  isa NQPArray
              - colonpair:  isa NQPArray
        - O:

Although the result of the two lines is the same, the parse trees look different, which is quite explainable. Instead of examining the parse trees, let us try locating the place where Perl 6 prints the string.

The say sub

This function is a multi-sub, which is defined in the src/core/io_operators.pm file in four different variants:

proto sub say(|) {*}
multi sub say() { . . . }
multi sub say(Junction:D \j) { . . . }
multi sub say(Str:D \x) { . . . }
multi sub say(\x) { . . . }

It should be quite logically that say 'Hello, World' is using the say(Str:D) function. To prove it, add a printing instruction as usual:

multi sub say(Str:D \x) {
    nqp::say('say(Str:D \x)');
    my $out := $*OUT;
    $out.print(nqp::concat(nqp::unbox_s(x),$out.nl-out));
}

Be very careful here not to type it like this:

say('say(Str:D \x)');

I did that mistake and faced an infinite loop that wanted all CPU and memory resources because our additional instruction used the same variant say(Str:D) for a defined string. Even more, the real printing never happened as the $out.print method is called a bit later and is never reached.

Using the nqp:: namespace easily bypasses the problem.

$ ./perl6 -e'say "Hello, World"'
say(Str:D \x)
Hello, World

The say method

Now, let’s try guessing where the say method can be located. I am talking about our second one-liner, 'Hello, World'.say. The first idea is to look for it in src/core/Str.pm, although you will not see it there.

The method is located in the grandgrandparent class Mu (Str←Cool←Any←Mu). You may be surprised to see how it looks like:

proto method say(|) {*}
multi method say() { say(self) }

The fact that it has a prototype and that it is a multi-sub, although there is only one implementation, is not that important now. What is interesting, is that the method barely calls the say sub, which we examined in the previous section.

Add another nqp::say to the method of Mu:

multi method say() { nqp::say('Mu.say()'); say(self) }

Now, run the second program:

$ ./perl6 -e'"Hello, World".say'
Mu.say()
say(Str:D \x)
Hello, World

As you see, we ended up in the same function. Although the difference between the two parse trees was quite big, the actual work was done by the same function in the end.

That’s all for today. Tomorrow, let’s examine other variants of the say sub.

🔬13. Let 1 + 2 * 3 = 9

Is it easy to break the behaviour of Perl 6? Well, the answer probably depends on what exactly you want to break.

Playing with operator precedence, I wanted to change the rules of arithmetical operators + and * so that they are executed in different order, namely, multiplication first, addition second.

Sounds like an easy task. Go to src/Perl6/Grammar.nqp and change a couple of lines that set the precedence of the + and * infixes:

- token infix:sym<*>    { <sym> <O(|%multiplicative)> }
+ token infix:sym<*>    { <sym> <O(|%additive)> }
. . .
- token infix:sym<+>    { <sym> <O(|%additive)> }
+ token infix:sym<+>    { <sym> <O(|%multiplicative)> }

Ready? Compile!

Recompiling the grammar takes a long time, so at first it looks promising, but after a few seconds, the compilation stops with an error:

Month out of range. Is: -935111296, should be in 1..12

Makefile:517: recipe for target 'perl6-m' failed
make: *** [perl6-m] Error 1

Month out of range?? Oh, we changed the rules of the Universe and before Perl 6 is even compiled, the new rules of arithmetics are already applied.

OK, let’s add some anaesthesia and suppress the error message. The code that checks for the correct month value is located in src/core/DateTime.pm, namely, inside the DateTime constructor. Comment that line out:

method !new-from-positional(DateTime:
    Int() $year,
    Int() $month,
    Int() $day,
    Int() $hour,
    Int() $minute,
        $second,
        %extra,
    :$timezone = 0,
    :&formatter,
) {
    # (1..12).in-range($month,'Month');
    (1 .. self.DAYS-IN-MONTH($year,$month)).in-range($day,'Day');
    (0..23).in-range($hour,'Hour');
    (0..59).in-range($minute,'Minute');
    (^61).in-range($second,'Second');
    . . .

This time, the month range check doesn’t stop us from going further but another error breaks in:

MVMArray: Index out of bounds

Makefile:517: recipe for target 'perl6-m' failed
make: *** [perl6-m] Error 1

Looks cryptic. MVMArray is a MoarVM array, obviously. So, we not only broke Perl 6 but MoarVM, too. Let’s go fix it.

The sources of MoarVM are located in a separate git repository at nqp/MoarVM. The message we saw can be found in nqp/MoarVM/src/6model/reprs/VMArray.c:

if (index < 0)
    MVM_exception_throw_adhoc(tc, "MVMArray: Index out of bounds");

There are two places like that, so let’s not guess which of them we need and preventatively change both of them to the following:

if (index < 0)
    index = 0;
    // MVM_exception_throw_adhoc(tc, "MVMArray: Index out of bounds");

(This is C, not Perl.)

From nqp/MoarVM, compile and re-install MoarVM and later try compiling Rakudo:

~/rakudo/nqp/MoarVM$ make
~/rakudo/nqp/MoarVM$ make install

~/rakudo/nqp/MoarVM$ cd ../..
~/rakudo$ make

This time, the error pops up immediately (as no NQP files are compiled):

Use of Nil in numeric context

Use of Nil in numeric context

Day out of range. Is: -51, should be in 1..0

Makefile:517: recipe for target 'perl6-m' failed
make: *** [perl6-m] Error 1

It looks like we can ignore Nils at the moment, but the DateTime hurts us again. We know the remedy:

# (1..12).in-range($month,'Month');
# (1 .. self.DAYS-IN-MONTH($year,$month)).in-range($day,'Day');

Yahoo! This time, the compilation process was calm and we got a new perl6 executable, which works as we wanted:

$ ./perl6 -e'say 1+2*3'
9

Don’t forget to restore the files before further experiments with Perl 6 🙂

Update

In the comment to this blog post, you can see a reference to the commit, which changes the way Rakudo checks the validity of the DateTime object. Instead of using the in-range method, simpler checks are used now, for example:

1 <= $month <= 12
    || X::OutOfRange.new(:what<Month>,:got($month),:range<1..12>).throw;

Here are the time measures of the two runs of a loop creating DateTime objects before and after the update:

time ./perl6 -e'DateTime.new(2018,1,5,12,30,0) for ^500000'
real 0m7.261s
user 0m7.276s
sys 0m0.020s

. . .

$ time ./perl6 -e'DateTime.new(2018,1,5,12,30,0) for ^500000'
real 0m4.457s
user 0m4.476s
sys 0m0.012s

🔬12. The beginning of the Grammar of Perl 6

Yesterday, we talked about the stages of the compiling process of a Perl 6 program and saw the parse tree of a simple ‘Hello, World!’ program. Today, our journey begins at the starting point of the Grammar.

So, here is the program:

say 'Hello, World!'

The grammar of Perl 6 is written in Not Quite Perl 6 and is located in Grammar.nqp 🙂 And that is amazing, as if you know how to work with grammars, you will be able to read the heart of the language.

The Perl 6 Grammar is defined as following:

grammar Perl6::Grammar is HLL::Grammar does STD {
    . . .
}

It is a class derived from HLL::Grammar (HLL stands for High-Level Language) and implements the STD (Standard) role. Let’s not focus on the hierarchy for now, though.

The Grammar has the TOP method. Notice that this is a method, not a rule or a token. The main feature of the method is that it is assumed that it contains some Perl 6 code, not regexes.

As we did earlier, let’s use our beloved method of reverse engineering by adding our own printing instructions to different places of Rakudo sources, recompiling it and watching how it works. The first target is the TOP method:

grammar Perl6::Grammar is HLL::Grammar does STD {
    my $sc_id := 0;
    method TOP() {
        nqp::say('At the TOP');
        . . .

As this is NQP, you need to call functions in the nqp:: namespace (although say is available without the namespace prefix, too). One of the notable differences between Perl 6 and NQP is the need to always have parentheses in function calls: if you omit them, the code won’t compile.

Perl inside regexes inside Perl

For training purposes, let’s try adding similar instruction to the comp_unit token (computational unit). This token is a part of the Grammar and is also called as one of the first methods during parsing Perl 6.

The body of the above shown TOP method is written in NQP. The body of a token is another language, and you should use regexes instead. Thus, to embed an instruction in Perl (or NQP), you need to switch the language.

There are two options: use a code block in curly braces or the colon-prefixed syntax that is very widely used in Rakudo sources to declare variables.

token comp_unit {
    {
        nqp::say('comp_unit');
    }
    :my $x := nqp::say('Var in grammar');
    . . .

Notice that it NQP, the binding := operator have to be used in place of the assignment =.

Statement list

So, back to the grammar. In the output that the --target=parse command-line option produces, we can see a statementlist node at the top of the parse tree. Let us look at its implementation in the Grammar. With some simplifications, it looks very lightweight:

rule statementlist($*statement_level = 0) {
    . . .
    <.ws>
    [
    | $
    | <?before <.[\)\]\}]>>
    | [ <statement> <.eat_terminator> ]*
    ]
    . . .
}

Basically, it says that a statement list is a list of zero or more statements. Square brackets in Perl 6 grammars create a non-capturing group, and we see three alternatives inside. One of the alternatives is just the end of data, another one is the end of the block (e. g., ending with a closing curly brace). For the sake of art, an additional vertical bar is added before the first alternative too.

The top-level rule is simple but the rest is becoming more and more complex. For example, let’s have a quick look at the eat terminator:

token eat_terminator {
    || ';'
    || <?MARKED('endstmt')> <.ws>
    || <?before ')' | ']' | '}' >
    || $
    || <?stopper>
    || <?before [if|while|for|loop|repeat|given|when] » > {
       $/.'!clear_highwater'(); self.typed_panic(
          'X::Syntax::Confused', reason => "Missing semicolon" ) }
    || { $/.typed_panic( 'X::Syntax::Confused', reason => "Confused" ) }
}

And this is just a small separator between the statements 🙂

The grammar file is more than 5500 lines of code; it is not possible to discuss and understand it all in a single blog post. Let us stop here for today and continue with easier stuff tomorrow.

🦋11. Compiler stages and targets in Perl 6

Welcome to the new year! Today, let us switch for a while from the discussion about obsolete messages to something different.

Stages

If you followed the exercises in the previous posts, you might have noticed that some statistics was printed in the console when compiling Rakudo:

Stage start      :   0.000
Stage parse      :  44.914
Stage syntaxcheck:   0.000
Stage ast        :   0.000
Stage optimize   :   4.245
Stage mast       :   9.476
Stage mbc        :   0.200

You could have also noticed that the bigger the file you changed, the slower it is compiled, up to dozens of seconds when you modify Grammar.pm.

It is also possible to see the statistics for your own programs. The --stagestats command-line option does the job:

$ ./perl6 --stagestats -e'say 42'
Stage start      :   0.000
Stage parse      :   0.065
Stage syntaxcheck:   0.000
Stage ast        :   0.000
Stage optimize   :   0.001
Stage mast       :   0.003
Stage mbc        :   0.000
Stage moar       :   0.000
42

So, let’s look at these stages. Roughly, half of them is about Perl 6, and half is about MoarVM. In the case Rakudo is configured to work with the JVM backend, the output will differ in the second half.

The Perl 6 part is clearly visible in the src/main.nqp file:

# Create and configure compiler object.
my $comp := Perl6::Compiler.new();
$comp.language('perl6');
$comp.parsegrammar(Perl6::Grammar);
$comp.parseactions(Perl6::Actions);
$comp.addstage('syntaxcheck', :before);
$comp.addstage('optimize', :after);
hll-config($comp.config);
nqp::bindhllsym('perl6', '$COMPILER_CONFIG', $comp.config);

Look at the selected lines. If you have played with Perl 6 Grammars, you know that big grammars are usually split into two parts: the grammar itself and the actions. The Perl 6 compiler does exactly the same thing for the Perl 6 grammar. There are two files: src/Perl6/Grammar.nqp and src/Perl6/Actions.nqp.

When looking at src/main.nqp, it is not quite clear that there are eight stages. Add the following line to the file:

for ($comp.stages()) { nqp::say($_) }

Now, recompile Rakudo and run any program:

$ ./perl6 -e'say 42'
start
parse
syntaxcheck
ast
optimize
mast
mbc
moar
42

Here they are.

The names of the first three stages—start, parse, and syntaxcheck—are quite self-explanatory. The ast stage is the stage of building an abstract syntax tree, which is then optimized in the optimize stage.

At this point, your Perl 6 program has been transformed into the abstract syntax tree and is about to be passed to the backend, MoarVM virtual machine in our case. The stages names start with m. The mast stage is the stage of the MoarVM assembly (not abstract) syntax tree, mbc stands for MoarVM bytecode and moar is when the VM executes the code.

Targets

Now that we know the stages of the Perl 6 program workflow, let’s make use of them. The --target option lets the compiler to stop at the given stage and display the result of it. This option supports the following values: parse, syntaxcheck, ast, optimize, and mast. With those options, Rakudo prints the output as a tree, and you can see how the program changes at different stages.

Even for small programs, the output, especially with the abstract syntax tree or an assembly tree of the VM is quite verbose. Let’s look at the parse tree of the ‘Hello, World!’ program, for example:

$ ./perl6 --target=parse -e'say "Hello, World!"'
- statementlist: say "Hello, World!"
  - statement: 1 matches
    - EXPR: say "Hello, World!"
      - args:  "Hello, World!"
        - arglist: "Hello, World!"
          - EXPR: "Hello, World!"
            - value: "Hello, World!"
              - quote: "Hello, World!"
                - nibble: Hello, World!
      - longname: say
        - name: say
          - identifier: say
          - morename:  isa NQPArray
        - colonpair:  isa NQPArray

All the names here correspond to rules, tokens, or methods of the Grammar. You can find them in src/Perl6/Grammar.nqp. As an exercise, try predicting if the name is a method, or a rule, or a token. Say, a value should be a token, as it is supposed to be a compact string, while a statementlist is a rule.

🔬10. Obsolete syntax error messages, part 2

Today, we continue exploring the error messages that Rakudo developers embedded to detect old Perl 5 constructions in the Perl 6 code.

The obs method

But first, let’s make a small experiment and add a call to the obs method in the rule parsing the for keyword.

rule statement_control:sym<for> {
    <sym><.kok> {}
    [ <?before 'my'? '$'\w+\s+'(' >
        <.obs('Hello', 'World!')> <.typed_panic: 'X::Syntax::P5'> ]?
    [ <?before '(' <.EXPR>? ';' <.EXPR>? ';' <.EXPR>? ')' >
        <.obs('C-style "for (;;)" loop', '"loop (;;)"')> ]?
    <xblock(1)>
}

The dot before the name of the method prevents creating a named element in the Match object. Actually, that is not that important as soon as the obs call generates an exception. In many other cases, the dot is very useful, of course.

Compile Rakudo and feed it with the erroneous Perl 6 code:

$ ./perl6 -e'for my $x (@a) {}'
===SORRY!=== Error while compiling -e
Unsupported use of Hello; in Perl 6 please use World!
at -e:1
------> for ⏏my $x (@a) {}

As you see, we’ve generated some rubbish message but the X::Syntax::P5 exception did not have a chance to appear, as the parsing stopped at the place the obs method was called.

No foreach anymore

Another error message appears when you try using the foreach keyword:

$ ./perl6 -e'foreach @a {}'
===SORRY!=== Error while compiling -e
Unsupported use of 'foreach'; in Perl 6 please use 'for'
at -e:1
------> foreach⏏ @a {}

Notice that the compiler stopped even before figuring out that the @a variable is not defined.

Here is the rule that finds the outdated keyword:

rule statement_control:sym<foreach> {
    <sym><.end_keyword> <.obs("'foreach'", "'for'")>
}

The end_keyword method is a token that matches the right edge of the keyword; this is not a method to report about the end of support of the keyword 🙂 You can see this method in many other rules in the grammar.

token end_keyword {
    » <!before <.[ \( \\ ' \- ]> || \h* '=>'>
}

No do anymore

Another potential mistake is creating the do blocks instead of the new repeat/while or repeat/until.

$ ./perl6 -e'do {} while 1'
===SORRY!=== Error while compiling -e
Unsupported use of do...while;
in Perl 6 please use repeat...while or repeat...until
at -e:1

This time, the logic for detecting the error is hidden deeply inside the statement token:

token statement($*LABEL = '') {
    . . .
    my $sp := $<EXPR><statement_prefix>;
    if $sp && $sp<sym> eq 'do' {
         my $s := $<statement_mod_loop><sym>;
         $/.obs("do..." ~ $s, "repeat...while or repeat...until");
    }
    . . .
}

The second symbol is taken from the $<statement_mod_loop><sym> value, so the error message contains the proper instruction for both do {} until and do {} for blocks.

Let’s stop here for today. We’ll examine more obsolete syntax in the next year. Meanwhile, I wish you all the best and success with using Perl 6 in 2018!

🔬9. Obsolete syntax error messages in Perl 6, part 1

Yesterday, we saw an error message about the improper syntax of the ternary operator. Let’s look at other similar things that the Rakudo designers has implemented for us to make the transition from Perl 5 smoother.

First of all, the Perl 6 grammar file (src/Perl6/Grammar.nqp) contains four different methods for reacting to obsolete syntax:

method obs($old, $new, $when = 'in Perl 6') {
    $*W.throw(self.MATCH(), ['X', 'Obsolete'],
        old         => $old,
        replacement => $new,
        when        => $when,
    );
}
method obsvar($name) {
    $*W.throw(self.MATCH(), ['X', 'Syntax', 'Perl5Var'], :$name);
}

method sorryobs($old, $new, $when = 'in Perl 6') {
    $*W.throw(self.MATCH(), ['X', 'Obsolete'],
        old         => $old,
        replacement => $new,
        when        => $when,
    );
}

method worryobs($old, $new, $when = 'in Perl 6') {
    self.typed_worry('X::Obsolete',
        old         => $old,
        replacement => $new,
        when        => $when,
    );
}

Three of these methods throw exceptions, the fourth one prints a warning. The final text of the error message is using the information from the arguments of the methods. For example, this is what we saw yesterday:

<.obs('? and : for the ternary conditional operator', '?? and !!')>

This part of the token regex is transformed to the following error message (the parts from the regex are highlighted):

Unsupported use of ? and : for the ternary conditional operator;
in Perl 6 please use ?? and !!

Obsolete syntax

Let us see what other messages we have in the current Rakudo Perl 6 compiler.

Negative indices

The first example is very likely one of the most common mistake that a Perl 5 programmer faces when programming in Perl 6.

$ perl6 -e'my @a; say @a[-1]'
===SORRY!=== Error while compiling -e
Unsupported use of a negative -1 subscript to index from the end;
in Perl 6 please use a function such as *-1
at -e:1
------> my @a; say @a[-1]⏏

To count from the end of the array, you should use a WhateverCode instead of negative integers. This is how the error message is encoded in the src/Perl6/Actions.nqp file (notice that this is an NQP module, not the Perl 6 one, while the syntax is very clear):

method postcircumfix:sym<[ ]>($/) {
    . . .
    my $ix := $_ ~~ / [ ^ | '..' ] \s* <( '-' \d+ )> \s* $ /;
    if $ix {
        $c.obs("a negative " ~ $ix ~ " subscript to index from the end", 
               "a function such as *" ~ $ix);
    }
    . . .
}

The $c variable is the current symbol in the syntax tree, and the $ix is a negative index taken from the square brackets (notice the position of the capturing parentheses inside the regex). If there is a negative index, an error message is generated for your pleasure.

The rest of the .obs calls happen in the src/Perl6/Grammar.nqp file.

Perl 6 loop, not C-style for

The for loop in Perl 6 is designed to work with lists or arrays, so using it in the C-style, which is allowed in Perl 5, is prohibited:

$ perl6 -e'for (my $i = 1; $i != 10; $i++) {}'
===SORRY!=== Error while compiling -e
Unsupported use of C-style "for (;;)" loop;
in Perl 6 please use "loop (;;)"
at -e:1
------> for ⏏(my $i = 1; $i != 10; $i++) {}

Localise that error message in the grammar:

rule statement_control:sym<for> {
    <sym><.kok> {}
    [ <?before 'my'? '$'\w+\s+'(' >
        <.typed_panic: 'X::Syntax::P5'> ]?
    [ <?before '(' <.EXPR>? ';' <.EXPR>? ';' <.EXPR>? ')' >
        <.obs('C-style "for (;;)" loop', '"loop (;;)"')> ]?
    <xblock(1)>
}

Here, you also can see another type of error message regarding the Perl 5 syntax (see where the typed_panic method matches):

$ ./perl6 -e'for my $x (@a) {}'
===SORRY!=== Error while compiling -e
This appears to be Perl 5 code
at -e:1
------> for ⏏my $x (@a) {}

Interestingly, this is the only place where the X::Syntax::P5 exception is used.

That’s all for today, stay tuned for more error messages tomorrow! 🙂

 

🔬8. Digging into operator precedence in Perl 6, part 2

Yesterday, we took a look at how the ? and so operators are dispatched depending on the type of the variable. We did it with the intention to understand what is the difference between them.

Here is once again an excerpt from the src/core/Bool.pm file, where the bodies of the subs look alike:

proto sub prefix:<?>(Mu $) is pure {*}
multi sub prefix:<?>(Bool:D \a) { a }
multi sub prefix:<?>(Bool:U \a) { Bool::False }
multi sub prefix:<?>(Mu \a) { a.Bool }

proto sub prefix:<so>(Mu $) is pure {*}
multi sub prefix:<so>(Bool:D \a) { a }
multi sub prefix:<so>(Bool:U \a) { Bool::False }
multi sub prefix:<so>(Mu \a) { a.Bool }

Both of them coerce the arguments to a Bool value. The difference is in their operator precedence. You cannot say for sure what is the precedence if you only look at the Bool.pm file. You will find more details in the src/Perl6/Grammar.nqp file describing the Perl 6 language grammar. Here are the fragments we need:

token prefix:sym<so> { <sym><.end_prefix> <O(|%loose_unary)> }
. . .
token prefix:sym<?> { <sym> <!before '??'> <O(|%symbolic_unary)> }

These look complex but let’s first concentrate only on the last part of the token definitions: <O(|%loose_unary)> and <O(|%symbolic_unary)>. Obviously, these are what define the rules for precedence. You can find a list of about 30 different kind of precedences in the same file:

## Operators

. . .
my %symbolic_unary := nqp::hash('prec', 'v=', 'assoc', 'unary', 'dba', 'symbolic unary');
. . .
my %list_assignment := nqp::hash('prec', 'i=', 'assoc', 'right', 'dba', 'list assignment', 'sub', 'e=', 'fiddly', 1);
my %loose_unary := nqp::hash('prec', 'h=', 'assoc', 'unary', 'dba', 'loose unary');
my %comma := nqp::hash('prec', 'g=', 'assoc', 'list', 'dba', 'comma', 'nextterm', 'nulltermish', 'fiddly', 1);
. . .

Let’s avoid digging deeper into how it works at the moment. Looking at the list you can guess that the letters k, j, h, and g define the preference order of different kinds of preference rules. As well a right or left dictate the associativity of the operators.

So, the so operator has the loose unary precedence level and the ? operator has a higher symbolic unary precedence.

The old conditional operator

Before we wrap up for today, let’s look at another interesting place where the single question mark can be caught in the Perl 6 program. I am talking about the following token in the grammar (notice that this time this is for an infix, not for a prefix):

token infix:sym<?> {
    <sym> {} <![?]> <?before <.-[;]>*?':'>
    <.obs('? and : for the ternary conditional operator', '?? and !!')>
    <O(|%conditional)>
}

This code catches the usage of a single ?, which was a part of the ternary operator in Perl 5 unlike the double ?? from the ternary operator in Perl 6.

The <.obs...> part of the token regex prints a warning about obsolete syntax:

$ ./perl6 -e'say 1 ? True : False'
===SORRY!=== Error while compiling -e
Unsupported use of ? and : for the ternary conditional operator;
in Perl 6 please use ?? and !!
at -e:1
------> say 1 ?⏏ True : False

So, if you use the old syntax, you’ll get not only an error message but also a hint on how to fix the issue.