File Coverage

blib/lib/Sim/Agent/Parser.pm
Criterion Covered Total %
statement 79 85 92.9
branch 27 34 79.4
condition 7 9 77.7
subroutine 7 7 100.0
pod 0 5 0.0
total 120 140 85.7


line stmt bran cond sub pod time code
1             package Sim::Agent::Parser;
2              
3 4     4   349754 use strict;
  4         9  
  4         135  
4 4     4   21 use warnings;
  4         7  
  4         3552  
5              
6             sub new
7             {
8 3     3 0 498354 bless {}, shift;
9             }
10              
11             sub parse_file
12             {
13 3     3 0 19 my ($self, $file) = @_;
14              
15 3 50       166 open my $fh, '<', $file or die "Cannot open $file";
16 3         21 local $/;
17 3         108 my $text = <$fh>;
18 3         36 close $fh;
19              
20 3         19 my $tokens = tokenize($text);
21 3         13 my $ast = parse($tokens);
22              
23 3         168 return $ast;
24             }
25              
26             ####################################
27              
28              
29             sub tokenize
30             {
31              
32 3     3 0 11 my ($text) = @_;
33              
34 3         8 my @tokens;
35 3         8 my $i = 0;
36 3         8 my $len = length($text);
37              
38 3         16 while ($i < $len)
39             {
40              
41 770         1295 my $char = substr($text, $i, 1);
42              
43             # skip whitespace
44 770 100       1826 if ($char =~ /\s/)
45             {
46 394         596 $i++;
47 394         847 next;
48             }
49              
50             # comment
51 376 100       713 if ($char eq ';')
52             {
53 1         2 $i++;
54 1   66     59 $i++ while $i < $len && substr($text,$i,1) ne "\n";
55 1         8 next;
56             }
57              
58             # left paren
59 375 100       761 if ($char eq '(')
60             {
61 100         340 push @tokens, { type => 'LPAREN' };
62 100         164 $i++;
63 100         208 next;
64             }
65              
66             # right paren
67 275 100       539 if ($char eq ')')
68             {
69 100         358 push @tokens, { type => 'RPAREN' };
70 100         152 $i++;
71 100         211 next;
72             }
73              
74             # string
75 175 100       347 if ($char eq '"')
76             {
77 32         54 $i++;
78 32         56 my $str = '';
79 32         66 while ($i < $len)
80             {
81 667         1136 my $c = substr($text,$i,1);
82              
83 667 50       1391 if ($c eq '\\')
84             {
85 0         0 $i++;
86 0         0 my $next = substr($text,$i,1);
87 0         0 $str .= $next;
88 0         0 $i++;
89 0         0 next;
90             }
91              
92 667 100       1377 last if $c eq '"';
93              
94 635         1032 $str .= $c;
95 635         1343 $i++;
96             }
97              
98 32 50       95 die "Unterminated string" if $i >= $len;
99              
100 32         60 $i++; # skip closing quote
101              
102 32         215 push @tokens, { type => 'STRING', value => $str };
103 32         84 next;
104             }
105              
106             # symbol
107 143         222 my $symbol = '';
108 143         290 while ($i < $len)
109             {
110 1162         1860 my $c = substr($text,$i,1);
111 1162 100 66     5147 last if $c =~ /\s/ || $c eq '(' || $c eq ')';
      100        
112 1019         1479 $symbol .= $c;
113 1019         1900 $i++;
114             }
115              
116 143         611 push @tokens, { type => 'SYMBOL', value => $symbol };
117             }
118              
119 3         14 return \@tokens;
120             }
121              
122              
123             sub parse
124             {
125              
126 3     3 0 9 my ($tokens) = @_;
127              
128 3         7 my $pos = 0;
129              
130 3         12 my $ast = parse_expr($tokens, \$pos);
131              
132 3 50       14 die "Extra tokens after root" if $pos < @$tokens;
133              
134 3         10 return $ast;
135             }
136              
137              
138             sub parse_expr
139             {
140              
141 275     275 0 496 my ($tokens, $pos_ref) = @_;
142              
143 275 50       549 my $token = $tokens->[$$pos_ref]
144             or die "Unexpected end of input";
145              
146             # list
147 275 100       585 if ($token->{type} eq 'LPAREN')
148             {
149              
150 100         164 $$pos_ref++;
151              
152 100         149 my @list;
153              
154 100         138 while (1) {
155              
156 372 50       771 my $next = $tokens->[$$pos_ref]
157             or die "Unclosed parenthesis";
158              
159 372 100       825 last if $next->{type} eq 'RPAREN';
160              
161 272         563 push @list, parse_expr($tokens, $pos_ref);
162             }
163              
164 100         146 $$pos_ref++; # skip RPAREN
165              
166 100         247 return \@list;
167             }
168              
169             # atom
170 175 100       357 if ($token->{type} eq 'STRING')
171             {
172 32         43 $$pos_ref++;
173 32         83 return $token->{value};
174             }
175              
176 143 50       288 if ($token->{type} eq 'SYMBOL')
177             {
178 143         195 $$pos_ref++;
179 143         431 return $token->{value};
180             }
181              
182 0           die "Unexpected token type: $token->{type}";
183             }
184              
185              
186             1;
187              
188              
189             =pod
190              
191             =head1 NAME
192              
193             Sim::Agent::Parser - Tokenizes and parses the S-expression plan into an AST
194              
195             =head1 DESCRIPTION
196              
197             Implements a strict S-expression parser with support for parentheses, symbols, double-quoted strings, whitespace, and semicolon comments.
198              
199             See L.
200              
201             =head1 AUTHOR
202              
203             Gian Luca Brunetti (2026), gianluca.brunetti@gmail.com
204              
205             =head1 LICENSE
206              
207             The GNU General Public License v3.0
208              
209             =cut
210              
211