File Coverage

blib/lib/Syntax/Keyword/Match/Deparse.pm
Criterion Covered Total %
statement 79 101 78.2
branch 24 44 54.5
condition 15 32 46.8
subroutine 9 10 90.0
pod 0 4 0.0
total 127 191 66.4


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2024 -- leonerd@leonerd.org.uk
5              
6             package Syntax::Keyword::Match::Deparse 0.15;
7              
8 1     1   1850 use v5.14;
  1         3  
9 1     1   6 use warnings;
  1         2  
  1         61  
10              
11 1     1   5 use B qw( opnumber OPf_KIDS OPf_STACKED );
  1         2  
  1         301  
12              
13             require B::Deparse;
14              
15             use constant {
16 1         194 OP_AND => opnumber('and'),
17             OP_COND_EXPR => opnumber('cond_expr'),
18             OP_CUSTOM => opnumber('custom'),
19             OP_ENTER => opnumber('enter'),
20             OP_LINESEQ => opnumber('lineseq'),
21             OP_MATCH => opnumber('match'),
22             OP_NULL => opnumber('null'),
23             OP_OR => opnumber('or'),
24             OP_PADSV => opnumber('padsv'),
25             OP_PADSV_STORE => opnumber('padsv_store'),
26             OP_SASSIGN => opnumber('sassign'),
27 1     1   7 };
  1         1  
28              
29             =head1 NAME
30              
31             C - L support for L
32              
33             =head1 DESCRIPTION
34              
35             Loading this module will apply some hacks onto L that attempts to
36             provide deparse support for code which uses the syntax provided by
37             L.
38              
39             =cut
40              
41             my $orig_pp_leave;
42             {
43 1     1   5 no warnings 'redefine';
  1         5  
  1         54  
44 1     1   5 no strict 'refs';
  1         1  
  1         1909  
45             $orig_pp_leave = *{"B::Deparse::pp_leave"}{CODE};
46             *{"B::Deparse::pp_leave"} = \&pp_leave;
47             }
48              
49             sub op_dump
50             {
51 0     0 0 0 my $o = shift;
52 0         0 my $ret = $o->name;
53              
54 0 0       0 my $kid = $o->flags & OPf_KIDS ? $o->first : undef;
55 0 0 0     0 if( $kid && !B::Deparse::null($kid) ) {
56 0         0 $ret .= "[\n";
57 0   0     0 while( $kid && !B::Deparse::null($kid) ) {
58 0         0 $ret .= join( "\n", map { " $_" } split m/\n/, op_dump($kid) ) . "\n";
  0         0  
59 0         0 $kid = $kid->sibling;
60             }
61 0         0 $ret .= "]";
62             }
63              
64 0         0 return $ret;
65             }
66              
67             my %operator_for_name = (
68             eq => "==",
69             seq => "eq",
70             match => "=~",
71             isa => "isa",
72             );
73              
74             sub operator_name
75             {
76 7     7 0 21 my ( $o ) = @_;
77 7         34 my $opname = $o->name;
78 7   50     36 return $operator_for_name{$opname} // die "TODO: operator name of $opname";
79             }
80              
81             sub is_match_on_topic
82             {
83 15     15 0 39 my ( $o, $topicix ) = @_;
84              
85 15 100       256 $o->type == OP_MATCH or return 0;
86              
87 2 50       24 if( $^V ge v5.22.0 ) {
    0          
88             # Perl 5.22 could do OP_MATCH on targ
89 2         50 return $o->targ == $topicix;
90             }
91             elsif( $o->flags & OPf_STACKED ) {
92 0         0 my $kid = $o->first;
93 0   0     0 return $kid->type == OP_PADSV && $kid->targ == $topicix;
94             }
95             else {
96 0         0 return 0;
97             }
98             }
99              
100             sub pp_leave
101             {
102 7     7 0 351772 my $self = shift;
103 7         18 my ( $op ) = @_;
104              
105 7         51 my $enter = $op->first;
106 7 50       42 $enter->type == OP_ENTER or
107             return $self->$orig_pp_leave( @_ );
108              
109 7         31 my $assign = $enter->sibling;
110 7         19 my $topicix; my $topicop;
111 7 50       124 if( $^V ge v5.38.0 ) {
112             # Since perl 5.38.0 we had OP_PADSV_STORE
113 7 50       48 $assign->type == OP_PADSV_STORE or
114             return $self->$orig_pp_leave( @_ );
115              
116 7         121 my $varname = $self->padname( $topicix = $assign->targ );
117 7 50       26 $varname eq '$(Syntax::Keyword::Match/topic)' or
118             return $self->$orig_pp_leave( @_ );
119              
120 7         37 $topicop = $assign->first;
121             }
122             else {
123             # Earlier perls had regular OP_SASSIGN with OP_PADSV target
124 0 0       0 $assign->type == OP_SASSIGN or
125             return $self->$orig_pp_leave( @_ );
126              
127 0         0 $topicop = $assign->first;
128              
129 0         0 my $padsvop = $topicop->sibling;
130 0 0       0 $padsvop->type == OP_PADSV or
131             return $self->$orig_pp_leave( @_ );
132              
133 0         0 my $varname = $self->padname( $topicix = $padsvop->targ );
134 0 0       0 $varname eq '$(Syntax::Keyword::Match/topic)' or
135             return $self->$orig_pp_leave( @_ );
136             }
137              
138 7         26 my $cmpop;
139             my @caseblocks;
140 7         32 my $kid = $assign->sibling;
141 7         90 while( !B::Deparse::null($kid) ) {
142 15 100       125 if( $kid->type == OP_NULL ) {
143 14         65 $kid = $kid->first;
144             }
145              
146 15 100       68 if( $kid->type == OP_LINESEQ ) {
147 1         223 push @caseblocks, "default {" . B::Deparse::scopeop( 1, $self, $kid, 0 ) . "}";
148 1         4 last;
149             }
150              
151 14         32 my ( $condop, $block );
152 14 100       79 if( $kid->type == OP_COND_EXPR ) {
    50          
153 8         33 $condop = $kid->first;
154 8         33 $block = $condop->sibling;
155 8         784 $kid = $block->sibling;
156             }
157             elsif( $kid->type == OP_AND ) {
158 6         40 $condop = $kid->first;
159 6         27 $block = $condop->sibling;
160 6         28 $kid = $block->sibling; # should be NULL
161             }
162             else {
163 0         0 warn op_dump($kid);
164 0         0 die "TODO: not sure how to handle kid=", $kid->name;
165             }
166              
167 14         29 my @cases;
168 14   66     163 while( $condop and !B::Deparse::null($condop) ) {
169 15 100       92 if( $condop->type == OP_NULL ) {
170 1         5 $condop = $condop->first;
171             }
172              
173 15         27 my $cond1;
174 15 100       58 if( $condop->type == OP_OR ) {
175 1         6 $cond1 = $condop->first;
176 1         5 $condop = $cond1->sibling;
177             }
178             else {
179 14         25 $cond1 = $condop;
180 14         29 $condop = undef;
181             }
182              
183 15         60 my $condlhs = $cond1->first;
184 15 100 66     59 if( is_match_on_topic( $cond1, $topicix ) ) {
    100 66        
185 2 50 66     18 die "Unsure how to handle mismatched case cond ops"
186             if $cmpop and $cmpop->type != $cond1->type;
187 2   66     13 $cmpop //= $cond1;
188             # Need to mangle out the target name
189 2         789 my $pattern = $self->deparse( $cond1, @_ );
190 2         31 $pattern =~ s{^\(\$\(Syntax::Keyword::Match/topic\) =~ (.*)\)$}{m$1};
191 2         17 push @cases, "case ($pattern)";
192             }
193             elsif( !B::Deparse::null($condlhs) and $condlhs->type == OP_PADSV and $condlhs->targ == $topicix ) {
194             # There's no way perl code could see the topic padname, so this
195             # must be a plain case(EXPR)
196 12         51 my $condval = $condlhs->sibling;
197              
198             # TODO: custom ops might be weird
199 12 50 66     67 die "Unsure how to handle mismatched case cond ops"
200             if $cmpop and $cmpop->type != $cond1->type;
201 12   66     50 $cmpop //= $cond1;
202              
203 12         736 push @cases, "case (" . $self->deparse( $condval, @_ ) . ")";
204             }
205             else {
206 1         230 my $cond = $self->deparse( $cond1, @_ );
207 1         10 $cond =~ s/^\((.*)\)$/$1/; # trim surrounding () so we don't get two
208 1         8 push @cases, "case if ($cond)";
209             }
210             }
211              
212 14         4221 push @caseblocks, join( ", ", @cases ) .
213             " {" . B::Deparse::scopeop( 1, $self, $block, 0 ) . "}";
214             }
215              
216 7         269 my $topic = $self->deparse( $topicop, @_ );
217              
218             # Ugh it'd be great if B::Deparse had a solution to this
219 7         30 my $cmp = operator_name( $cmpop );
220              
221 7         968 return "match ($topic : $cmp) {\n\t" . join( "\n", @caseblocks ) . "\n\b}";
222             }
223              
224             =head1 TODO
225              
226             =over 4
227              
228             =item *
229              
230             Integrate with custom ops (C, C, etc..)
231              
232             =item *
233              
234             Handle the experimental dispatch op feature
235              
236             =back
237              
238             =cut
239              
240             =head1 AUTHOR
241              
242             Paul Evans
243              
244             =cut
245              
246             0x55AA;