File Coverage

blib/lib/MarpaX/Repa/Lexer.pm
Criterion Covered Total %
statement 82 114 71.9
branch 34 82 41.4
condition 15 28 53.5
subroutine 8 9 88.8
pod 6 6 100.0
total 145 239 60.6


line stmt bran cond sub pod time code
1 1     1   28 use 5.010;
  1         4  
  1         44  
2 1     1   5 use strict;
  1         2  
  1         35  
3 1     1   5 use warnings;
  1         2  
  1         1573  
4              
5             package MarpaX::Repa::Lexer;
6             our $VERSION = '0.01';
7              
8             =head1 NAME
9              
10             MarpaX::Repa::Lexer - simplify lexing for Marpa parser
11              
12             =head1 DESCRIPTION
13              
14             Most details are in L.
15              
16             =head1 METHODS
17              
18             =head2 new
19              
20             Returns a new lexer instance. Takes named arguments.
21              
22             my $lexer = MyLexer->new(
23             tokens => {
24             word => qr{\b\w+\b},
25             },
26             store => 'array',
27             debug => 1,
28             );
29              
30             Possible arguments:
31              
32             =over 4
33              
34             =item tokens
35              
36             Hash with names of terminals as keys and one of the
37             following as value:
38              
39             =over 4
40              
41             =item string
42              
43             Just a string to match.
44              
45             'a token' => "matches this long string",
46              
47             =item regular expression
48              
49             A C compiled regexp.
50              
51             'a token' => qr{"[^"]+"},
52              
53             Note that regexp MUST match at least one character. At this moment
54             look behind to look at chars before the current position is not
55             supported.
56              
57             =item hash
58              
59             With hash you can define token specific options. At this moment
60             'store' option only (see below). Use C key to set what to
61             match (string or regular expression):
62              
63             'a token' => {
64             match => "a string",
65             store => 'hash',
66             },
67              
68             Per token options are:
69              
70             =over 4
71              
72             =item store
73              
74             What to store (pass as value to Marpa's recognizer). The following variants
75             are supported:
76              
77             =over 4
78              
79             =item hash (default)
80              
81             { token => 'a token', value => 'a value' }
82              
83             =item array
84              
85             [ 'a token', 'a value' ]
86              
87             =item scalar
88              
89             'a value'
90              
91             =item undef
92              
93             undef is stored so later Repa's actions will skip it.
94              
95             =item a callback
96              
97             A function will be called with token name and reference to its value.
98             Should return a reference or undef that will be passed to recognizer.
99              
100             =back
101              
102             =item check
103              
104             A callback that can check whether token is really match or not.
105              
106             =back
107              
108             =back
109              
110             =item debug
111              
112             If true then lexer prints debug log to STDERR.
113              
114             =item min_buffer
115              
116             Minimal size of the buffer (4*1024 by default).
117              
118             =back
119              
120             =cut
121              
122             sub new {
123 5     5 1 11 my $proto = shift;
124 5   33     43 my $self = bless { @_ }, ref $proto || $proto;
125 5         16 return $self->init;
126             }
127              
128             =head2 init
129              
130             Setups instance and returns C<$self>. Called from constructor.
131              
132             =cut
133              
134             sub init {
135 5     5 1 7 my $self = shift;
136              
137 5         13 my $tokens = $self->{'tokens'};
138 5         22 foreach my $token ( keys %$tokens ) {
139 5         8 my ($match, @rest);
140 5 50       26 if ( ref( $tokens->{ $token } ) eq 'HASH' ) {
141 0         0 $match = $tokens->{ $token }{'match'};
142 0         0 @rest = (@{ $tokens->{ $token } }{'store','check'});
  0         0  
143             } else {
144 5         35 $match = $tokens->{ $token };
145             }
146 5   50     34 $rest[0] ||= $self->{'store'} || 'hash';
      33        
147 5 0       12 my $type =
    50          
148             ref $match ? 'RE'
149             : length $match == 1 ? 'CHAR'
150             : 'STRING';
151 5         22 $tokens->{ $token } = [ $type, $match, @rest ];
152             }
153              
154 5   50     26 $self->{'min_buffer'} //= 4*1024;
155 5   50     23 $self->{'buffer'} //= '';
156              
157 5         15 return $self;
158             }
159              
160             =head2 recognize
161              
162             Takes a recognizer and a file handle. Parses input. Dies on critical errors, but
163             not when parser lost its way. Returns recognizer that was passed.
164              
165             =cut
166              
167             sub recognize {
168 5     5 1 6 my $self = shift;
169 5         7 my $rec = shift;
170 5         7 my $fh = shift;
171              
172              
173 5         13 my $buffer = $self->buffer;
174 5         14 my $buffer_can_grow = $self->grow_buffer( $fh );
175              
176 5         21 my $expected = $rec->terminals_expected;
177 5 50       134 return $rec unless @$expected;
178              
179 5         15 while ( length $$buffer ) {
180 7 50       16 say STDERR "Expect token(s): ". join(', ', map "'$_'", @$expected)
181             if $self->{'debug'};
182              
183 7 50       16 say STDERR "Buffer start: ". $self->dump_buffer .'...'
184             if $self->{'debug'};
185              
186 7         11 my $longest = 0;
187 7         9 my @alternatives;
188 7         14 my $first_char = substr $$buffer, 0, 1;
189 7         28 foreach my $token ( @$expected ) {
190 9         11 REDO:
191              
192             my ($matched, $match, $length);
193 9 50       13 my ($type, $what, $how, $check) = @{ $self->{'tokens'}{ $token } || [] };
  9         45  
194              
195 9 50       31 unless ( $type ) {
    50          
    0          
    0          
196 0 0       0 say STDERR "Unknown token: '$token'" if $self->{'debug'};
197 0         0 next;
198             }
199             elsif ( $type eq 'RE' ) {
200 9 100       96 if ( $$buffer =~ /^($what)/ ) {
201 7         25 ($matched, $match, $length) = (1, $1, length $1);
202 7 100 100     34 if ( $length == length $$buffer && $buffer_can_grow ) {
203 2         5 $buffer_can_grow = $self->grow_buffer( $fh );
204 2         25 goto REDO;
205             }
206             }
207             }
208             elsif ( $type eq 'STRING' ) {
209 0         0 $length = length $what;
210 0 0       0 ($matched, $match) = (1, $what)
211             if $what eq substr $$buffer, 0, $length;
212             }
213             elsif ( $type eq 'CHAR' ) {
214 0 0       0 ($matched, $match, $length) = (1, $first_char, 1)
215             if $what eq $first_char;
216             }
217             else {
218 0         0 die "Unknown type $type";
219             }
220              
221 7 100       16 unless ( $matched ) {
222 2 50       6 say STDERR "No '$token' in ". $self->dump_buffer if $self->{'debug'};
223 2         7 next;
224             }
225              
226 5 50       10 unless ( $length ) {
227 0         0 die "Token '$token' matched empty string. This is not supported.";
228             }
229 5 50       12 say STDERR "Token '$token' matched ". $self->dump_buffer( $length )
230             if $self->{'debug'};
231              
232 5 50 33     14 if ( $check && !$check->( $self, $token, $match, $length ) ) {
233 0 0       0 say STDERR "\tCheck failed for '$token', skipping"
234             if $self->{'debug'};
235 0         0 next;
236             }
237              
238 5 50       12 if ( $self->{longest_expected} ) {
239 0 0       0 if ( $length > $longest ) {
    0          
240 0 0       0 say STDERR "New longest token of length $length" if $self->{'debug'};
241 0         0 @alternatives = (); $longest = $length;
  0         0  
242             } elsif ( $length < $longest ) {
243 0 0       0 say STDERR "Skipping $token token as it's short" if $self->{'debug'};
244 0         0 next;
245             }
246             }
247 5         25 push @alternatives, [$token, $how, $match, $length];
248             }
249              
250 7         15 foreach my $e ( @alternatives ) {
251 5         19 my ($token, $how, $match, $length) = @$e;
252 5 50       14 say STDERR "Accepting $token of length $length" if $self->{'debug'};
253              
254 5 50       26 if ( ref $how ) {
    50          
    50          
    50          
    0          
255 0         0 $match = $how->( $token, \"$match" );
256             } elsif ( $how eq 'hash' ) {
257 0         0 $match = \{ token => $token, value => $match };
258             } elsif ( $how eq 'array' ) {
259 0         0 $match = \[$token, $match];
260             } elsif ( $how eq 'scalar' ) {
261 5         12 $match = \"$match";
262             } elsif ( $how eq 'undef' ) {
263 0         0 $match = \undef;
264             } else {
265 0         0 die "Unknown store variant - '$how'";
266             }
267              
268 5         21 $rec->alternative( $token, $match, $length );
269             }
270              
271 7         265 my $skip = 0;
272 7         8 while (1) {
273             # XXX: we are done, no way to advance further, we would love this
274             # to be improved in Marpa
275 8 100       24 if ( $rec->current_earleme == $rec->thin->furthest_earleme ) {
276 2         24 return $rec;
277             }
278              
279 6         66 $skip++;
280 6         8 local $@;
281 6 50       11 if ( defined (my $events = eval { $rec->earleme_complete }) ) {
  6         18  
282 6 100 66     218 if ( $events && $rec->exhausted ) {
283 3         29 substr $$buffer, 0, $skip, '';
284 3         15 return $rec;
285             }
286 3         9 $expected = $rec->terminals_expected;
287 3 100       52 last if @$expected;
288             } else {
289 0 0       0 say STDERR "Failed to parse: $@" if $self->{'debug'};
290 0         0 return $rec;
291             }
292             }
293 2         5 substr $$buffer, 0, $skip, '';
294 2 100 66     13 $buffer_can_grow = $self->grow_buffer( $fh )
295             if $buffer_can_grow && $self->{'min_buffer'} > length $$buffer;
296              
297 2 50       12 say STDERR '' if $self->{'debug'};
298             }
299 0         0 return $rec;
300             }
301              
302             =head2 buffer
303              
304             Returns reference to the current buffer.
305              
306             =cut
307              
308 10     10 1 4424 sub buffer { \$_[0]->{'buffer'} }
309              
310             =head2 grow_buffer
311              
312             Called when L needs a re-fill with a file handle as argument.
313             Returns true if there is still data to come from the handle.
314              
315             =cut
316              
317             sub grow_buffer {
318 8     8 1 12 my $self = shift;
319 8         39 local $/ = \($self->{'min_buffer'}*2);
320 8   100     68 $self->{'buffer'} .= readline($_[0]) // return 0;
321 5         19 return 1 && $self->{'min_buffer'};
322             }
323              
324             =head2 dump_buffer
325              
326             Returns first 20 chars of the buffer with everything besides ASCII encoded
327             with C<\x{####}>. Use argument to control size, zero to mean whole buffer.
328              
329             =cut
330              
331             sub dump_buffer {
332 0     0 1   my $self = shift;
333 0   0       my $show = shift // 20;
334 0 0         my $str = $show? substr( $self->{'buffer'}, 0, $show ) : $self->{'buffer'};
335 0           (my $res = $str) =~ s/([^\x20-\x7E])/'\\x{'. hex( ord $1 ) .'}' /ge;
  0            
336 0           return $res;
337             }
338              
339             1;