Reduction operators are one of the many attractive features of Perl 6. A classical example is calculating factorial:
say [*] 1..5; # 120
It is remarkable that in the AST output (generated with the --target=ast command-line option) you do not see any cycles. There is the METAOP_REDUCE_LEFT call, and obviously, the rest is hidden on the deeper levels.
- QAST::Stmts
- QAST::WVal(Array)
- QAST::Stmts <sunk> say [*] 1..5
- QAST::Stmt <sunk final> say [*] 1..5
- QAST::Want <sunk>
- QAST::Op(call &say) <sunk> :statement_id<?> say [*] 1..5
- QAST::Op(call) <wanted> [*] 1..5
- QAST::Op(call &METAOP_REDUCE_LEFT) <wanted>
- QAST::Var(lexical &infix:<*>) <wanted>
- QAST::Op(call &infix:<..>) <wanted> ..
- QAST::Want <wanted> 1
- QAST::WVal(Int)
- Ii
- QAST::IVal(1)
- QAST::Want <wanted> 5
- QAST::WVal(Int)
- Ii
- QAST::IVal(5)
Nevertheless, let us at least look at the Grammar and see how it handles the reduction operator.
regex term:sym<reduce> { :my $*IN_REDUCE := 1; :my $op; <?before '['\S+']'> <!before '[' <.[ - + ? ~ ^ ]> <.[ \w $ @ ]> > # disallow accidental prefix before termish thing '[' [ || <op=.infixish('red')> <?[\]]> || $<triangle>=[\\]<op=.infixish('tri')> <?[\]]> || <!> ] ']' { $op := $<op> } <.can_meta($op, "reduce with")> [ || <!{ $op<OPER><O>.made<diffy> }> || <?{ $op<OPER><O>.made<pasttype> eq 'chain' }> || { self.typed_panic: "X::Syntax::CannotMeta", meta => "reduce with", operator => ~$op<OPER><sym>, dba => ~$op<OPER><O>.made<dba>, reason => 'diffy and not chaining' } ] { $*IN_REDUCE := 0 } <args> }
The regex needs a pair of square brackets (shown in blue) and an operator between them. The operator is saved in $<op> but also in the $op local variable: notice how you can use a colon to declare variables inside the regex rules.
Then, the operator is checked if it can be reduced (.can_meta), and finally, some arguments are parsed. In our case, the <args> rule should match with 1..5.
What happens in-between with all those diffy and pasttype, is not clear for me. But notice how a dynamic variable $*IN_REDUCE is used as a flag so that inner rules understand that they are parsing something inside the reduction meta-operator.
Further adventures of the reduction story are even less clear. Let us just take a brief look at the corresponding action (actually, to its first part):
method term:sym<reduce>($/) { my $base := $<op>; my $basepast := $base.ast ?? $base.ast[0] !! QAST::Var.new(:name("&infix" ~ $*W.canonicalize_pair('', $base<OPER><sym>)), :scope<lexical>); my $metaop := baseop_reduce($base<OPER><O>.made); my $metapast := QAST::Op.new( :op<call>, :name($metaop), WANTED($basepast,'reduce')); my $t := $basepast.ann('thunky') || $base<OPER><O>.made<thunky>; if $<triangle> { $metapast.push($*W.add_constant('Int', 'int', 1)); } my $args := $<args>.ast; # one-arg rule? if +$args.list == 1 && !$args[0].flat && !$args[0].named { make QAST::Op.new(:node($/), :op<call>, WANTED($metapast,'reduce/meta'), WANTED($args[0],'reduce/meta')); } . . . }
Everything ends with generating an item in QAST.
(By the way, did you know why the name starts with ‘Q’? Originally, it was PAST, short for Parrot AST. Then, a newer version of the tree appeared, and the next letter from the alphabet was used instead.)
One thought on “🔬52. An attempt to understand how [*] works in Perl 6”