File Coverage

blib/lib/Lox/Scanner.pm
Criterion Covered Total %
statement 64 170 37.6
branch 49 108 45.3
condition 4 20 20.0
subroutine 16 29 55.1
pod 0 25 0.0
total 133 352 37.7


line stmt bran cond sub pod time code
1             package Lox::Scanner;
2 1     1   6 use strict;
  1         1  
  1         26  
3 1     1   4 use warnings;
  1         2  
  1         20  
4 1     1   5 use Lox::TokenType;
  1         1  
  1         119  
5 1     1   322 use Lox::Token;
  1         2  
  1         1916  
6             our $VERSION = 0.02;
7              
8             sub new {
9 2     2 0 4 my ($class, $args) = @_;
10             return bless {
11             source => $args->{source},
12 2         12 tokens => [],
13             current => 0,
14             column => 0,
15             line => 1,
16             }, $class;
17             }
18              
19 2     2 0 8 sub tokens { $_[0]->{tokens} }
20              
21             sub print {
22 0     0 0 0 my ($self) = @_;
23 0         0 for ($self->{tokens}->@*) {
24             printf "% 3d % 3d % -15s %s\n",
25 0         0 $_->{line}, $_->{column}, Lox::TokenType::type($_->{type}), $_->{lexeme};
26             }
27             }
28              
29             sub scan_tokens {
30 2     2 0 3 my $self = shift;
31 2         6 while (!$self->is_at_end) {
32 12         20 $self->scan_token;
33             }
34             # handle non-terminated input
35 2 50 33     9 unless ($self->{tokens}->@* && $self->{tokens}[-1]->type eq EOF) {
36 2         6 $self->chomp_eof('');
37             }
38             }
39              
40             sub scan_token {
41 12     12 0 15 my $self = shift;
42 12         16 my $c = $self->advance;
43              
44 12 50       103 if ($c eq '') {
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    100          
    50          
    50          
    50          
    100          
    50          
    50          
    50          
    100          
    50          
    100          
    50          
45 0         0 $self->chomp_eof($c);
46             }
47             elsif ($c eq '(') {
48 0         0 $self->add_token(lexeme=>$c, type=>LEFT_PAREN);
49             }
50             elsif ($c eq ')') {
51 0         0 $self->add_token(lexeme=>$c, type=>RIGHT_PAREN);
52             }
53             elsif ($c eq '{') {
54 0         0 $self->add_token(lexeme=>$c, type=>LEFT_BRACE);
55             }
56             elsif ($c eq '}') {
57 0         0 $self->add_token(lexeme=>$c, type=>RIGHT_BRACE);
58             }
59             elsif ($c eq ',') {
60 0         0 $self->add_token(lexeme=>$c, type=>COMMA);
61             }
62             elsif ($c eq '.') {
63 0         0 $self->add_token(lexeme=>$c, type=>DOT);
64             }
65             elsif ($c eq '-') {
66 0         0 $self->add_token(lexeme=>$c, type=>MINUS);
67             }
68             elsif ($c eq '+') {
69 0         0 $self->add_token(lexeme=>$c, type=>PLUS);
70             }
71             elsif ($c eq ';') {
72 2         3 $self->add_token(lexeme=>$c, type=>SEMICOLON);
73             }
74             elsif ($c eq '*') {
75 0         0 $self->add_token(lexeme=>$c, type=>STAR);
76             }
77             elsif ($c eq '!') {
78 0         0 $self->chomp_bang($c);
79             }
80             elsif ($c eq '.') {
81 0         0 $self->chomp_method($c);
82             }
83             elsif ($c eq '=') {
84 1         71 $self->chomp_equal($c);
85             }
86             elsif ($c eq '>') {
87 0         0 $self->chomp_greater($c);
88             }
89             elsif ($c eq '<') {
90 0         0 $self->chomp_less($c);
91             }
92             elsif ($c eq '/') {
93 0         0 $self->chomp_slash($c);
94             }
95             elsif ($c eq '"') {
96 1         4 $self->chomp_string($c);
97             }
98             elsif ($c =~ /\d/) {
99 0         0 $self->chomp_number($c);
100             }
101             elsif ($c =~ /\w/) {
102 4         10 $self->chomp_identifier($c);
103             }
104             elsif ($c =~ /\s/) {
105             # whitespace no-op
106             }
107             else {
108 0         0 $self->lex_error($c);
109             }
110             }
111              
112             sub is_at_end {
113 19     19 0 22 my $self = shift;
114 19         45 return $self->{current} >= length $self->{source};
115             }
116              
117             sub match {
118 0     0 0 0 my ($self, $expected_char) = @_;
119 0 0 0     0 if (!$self->is_at_end && $self->peek eq $expected_char) {
120 0         0 $self->{current}++;
121 0         0 return 1;
122             }
123 0         0 return undef;
124             }
125              
126              
127             sub advance {
128 26     26 0 28 my $self = shift;
129 26         44 my $next = substr $self->{source}, $self->{current}++, 1;
130 26 50       38 if ($next eq "\n") {
131 0         0 $self->{column} = 0;
132 0         0 $self->{line}++;
133             }
134             else {
135 26         27 $self->{column}++;
136             }
137 26         39 return $next;
138             }
139              
140             sub peek {
141 19     19 0 29 my ($self, $length) = @_;
142 19   50     79 return substr $self->{source}, $self->{current}, $length||1;
143             }
144              
145             sub chomp_eof {
146 2     2 0 3 my ($self, $c) = @_;
147 2         5 $self->add_token(lexeme=>$c, type=>EOF);
148             }
149              
150             sub chomp_left_brace {
151 0     0 0 0 my ($self, $c) = @_;
152             }
153              
154             sub chomp_right_brace {
155 0     0 0 0 my ($self, $c) = @_;
156 0         0 $self->add_token(lexeme=>$c, type=>RIGHT_BRACE);
157             }
158             sub chomp_left_paren {
159 0     0 0 0 my ($self, $c) = @_;
160 0         0 $self->add_token(lexeme=>$c, type=>LEFT_PAREN);
161             }
162              
163             sub chomp_right_paren {
164 0     0 0 0 my ($self, $c) = @_;
165 0         0 $self->add_token(lexeme=>$c, type=>RIGHT_PAREN);
166             }
167              
168             sub chomp_identifier {
169 4     4 0 6 my ($self, $c) = @_;
170 4         6 my $column = $self->{column};
171 4         8 while ($self->peek =~ /[A-Za-z0-9_]/) {
172 10         20 $c .= $self->advance;
173             }
174 4         7 my $type;
175 4 50       34 if ($c eq 'and') {
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    100          
    50          
    50          
    50          
    50          
    100          
    50          
176 0         0 $type = AND;
177             }
178             elsif ($c eq 'break') {
179 0         0 $type = BREAK;
180             }
181             elsif ($c eq 'class') {
182 0         0 $type = CLASS;
183             }
184             elsif ($c eq 'else') {
185 0         0 $type = ELSE;
186             }
187             elsif ($c eq 'false') {
188 0         0 $type = FALSE;
189             }
190             elsif ($c eq 'for') {
191 0         0 $type = FOR;
192             }
193             elsif ($c eq 'fun') {
194 0         0 $type = FUN;
195             }
196             elsif ($c eq 'if') {
197 0         0 $type = IF;
198             }
199             elsif ($c eq 'nil') {
200 0         0 $type = NIL;
201             }
202             elsif ($c eq 'or') {
203 0         0 $type = OR;
204             }
205             elsif ($c eq 'print') {
206 1         2 $type = PRINT;
207             }
208             elsif ($c eq 'return') {
209 0         0 $type = RETURN;
210             }
211             elsif ($c eq 'super') {
212 0         0 $type = SUPER;
213             }
214             elsif ($c eq 'this') {
215 0         0 $type = THIS;
216             }
217             elsif ($c eq 'true') {
218 0         0 $type = TRUE;
219             }
220             elsif ($c eq 'var') {
221 1         3 $type = VAR;
222             }
223             elsif ($c eq 'while') {
224 0         0 $type = WHILE;
225             }
226             else {
227 2         3 $type = IDENTIFIER;
228             }
229 4         9 $self->add_token(lexeme=>$c, column=>$column, type=>$type);
230             }
231              
232             sub chomp_class {
233 0     0 0 0 my ($self, $word) = @_;
234 0         0 my $column = $self->{column};
235 0         0 while ($self->peek =~ /[\w\d]/) {
236 0         0 $word .= $self->advance;
237             }
238 0         0 $self->add_token(lexeme=>$word, column=>$column, type=>CLASS);
239             }
240              
241              
242             sub chomp_number {
243 0     0 0 0 my ($self, $c) = @_;
244 0         0 my $column = $self->{column};
245 0         0 $c .= $self->advance while ($self->peek =~ /\d/);
246 0 0 0     0 if ($self->peek eq '.' && $self->peek(2) =~ /\d/) {
247 0         0 $c .= $self->advance;
248 0         0 $c .= $self->advance while ($self->peek =~ /\d/);
249             }
250 0         0 $self->add_token(lexeme=>$c, type=>NUMBER, literal=>$c, column=>$column);
251             }
252              
253             sub chomp_string {
254 1     1 0 2 my ($self, $c) = @_;
255 1         2 my $column = $self->{column};
256 1         1 my $word = '';
257              
258 1   66     3 while (!$self->is_at_end && $self->peek ne '"') {
259 3         6 my $next = $self->advance;
260 3 50       5 if ($next eq '\\') { # handle \"
261 0         0 $next .= $self->advance;
262             }
263 3         7 $word .= $next;
264             }
265 1 50       4 if ($self->is_at_end) {
266 0         0 $self->lex_error('EOF', 'Unterminated string');
267 0         0 return;
268             }
269 1         3 $self->advance;
270 1         5 $self->add_token(
271             lexeme=>"\"$word\"", type=>STRING, literal=>$word, column=>$column);
272             }
273              
274             sub chomp_bang {
275 0     0 0 0 my ($self, $c) = @_;
276 0         0 my $type = BANG;
277 0 0       0 if ($self->peek eq '=') {
278 0         0 $c .= $self->advance;
279 0         0 $type = BANG_EQUAL;
280             }
281 0         0 $self->add_token(lexeme => $c, type => $type);
282             }
283              
284             sub chomp_equal {
285 1     1 0 4 my ($self, $c) = @_;
286 1         2 my $type = EQUAL;
287 1 50       3 if ($self->peek eq '=') {
288 0         0 $c .= $self->advance;
289 0         0 $type = EQUAL_EQUAL;
290             }
291 1         4 $self->add_token(lexeme => $c, type => $type);
292             }
293              
294             sub chomp_greater {
295 0     0 0 0 my ($self, $c) = @_;
296 0         0 my $type = GREATER;
297 0 0       0 if ($self->peek eq '=') {
298 0         0 $c .= $self->advance;
299 0         0 $type = GREATER_EQUAL;
300             }
301 0         0 $self->add_token(lexeme => $c, type => $type);
302             }
303              
304             sub chomp_less {
305 0     0 0 0 my ($self, $c) = @_;
306 0         0 my $type = LESS;
307 0 0       0 if ($self->peek eq '=') {
308 0         0 $c .= $self->advance;
309 0         0 $type = LESS_EQUAL;
310             }
311 0         0 $self->add_token(lexeme => $c, type => $type);
312             }
313              
314             sub chomp_slash {
315 0     0 0 0 my ($self, $c) = @_;
316 0 0       0 if ($self->peek eq '/') { # single-line comment
    0          
317 0   0     0 while (!$self->is_at_end && $self->peek ne "\n") {
318 0         0 $self->advance;
319             }
320             }
321             elsif ($self->peek eq '*') { # multi-line comment
322 0         0 my $multiline = 1;
323 0         0 while (!$self->is_at_end) {
324 0 0       0 if ($self->peek(2) eq '*/') {
    0          
325 0         0 $multiline--;
326 0         0 $self->advance;
327 0         0 $self->advance;
328             }
329             elsif ($self->peek(2) eq '/*') {
330 0         0 $multiline++;
331 0         0 $self->advance;
332 0         0 $self->advance;
333             }
334 0 0       0 last unless $multiline;
335 0         0 $self->advance;
336             }
337 0 0       0 if ($multiline) {
338 0         0 $self->lex_error('EOF', 'Unterminated multi-line comment');
339             }
340             }
341             else {
342 0         0 $self->add_token(lexeme => $c, type => SLASH);
343             }
344             }
345              
346             sub add_token {
347 10     10 0 25 my ($self, %args) = @_;
348             push $self->{tokens}->@*, Lox::Token->new(
349             {
350             literal => undef,
351             column => $self->{column},
352             line => $self->{line},
353 10         57 %args,
354             });
355             }
356              
357             sub lex_error {
358 0     0 0   my ($self, $c, $msg) = @_;
359 0   0       $msg //= "unexpected character: $c";
360             my $t = Lox::Token->new({
361             literal => undef,
362             lexeme => $c,
363             column => $self->{column},
364             line => $self->{line},
365 0           type => ERROR});
366 0           Lox::error($t, $msg);
367             }
368              
369             1;