File Coverage

blib/lib/XS/JIT/Header/Parser.pm
Criterion Covered Total %
statement 136 152 89.4
branch 38 58 65.5
condition 7 9 77.7
subroutine 18 22 81.8
pod 11 11 100.0
total 210 252 83.3


line stmt bran cond sub pod time code
1             package XS::JIT::Header::Parser;
2              
3 4     4   113249 use strict;
  4         5  
  4         151  
4 4     4   15 use warnings;
  4         7  
  4         206  
5 4     4   20 use File::Temp qw(tempfile);
  4         7  
  4         223  
6 4     4   19 use File::Spec;
  4         10  
  4         12614  
7              
8             our $VERSION = '0.17';
9              
10             # Find a working C preprocessor
11             sub _find_preprocessor {
12 25     25   120 my @candidates = qw(clang cpp gcc);
13              
14 25         92 for my $cmd (@candidates) {
15 50         254983 my $output = `$cmd --version 2>/dev/null`;
16 50 100       1343 if ($? == 0) {
17 25 50       928 return $cmd eq 'gcc' ? "$cmd -E" : "$cmd -E";
18             }
19             }
20              
21 0         0 die "No C preprocessor found. Install clang, gcc, or cpp.";
22             }
23              
24             # Create a new parser instance
25             sub new {
26 28     28 1 196723 my ($class, %opts) = @_;
27              
28             my $self = bless {
29             include => $opts{include} || [],
30             define => $opts{define} || {},
31 28   100     382 functions => {},
      100        
32             constants => {},
33             typedefs => {},
34             enums => {},
35             structs => {},
36             _raw_code => '',
37             _preprocessed => '',
38             }, $class;
39              
40 28         94 return $self;
41             }
42              
43             # Parse a header file
44             sub parse_file {
45 14     14 1 51 my ($self, $header_path) = @_;
46              
47 14 50       33 die "Header file required" unless defined $header_path;
48              
49             # Read raw header for constant extraction
50 14 50       133 if (-f $header_path) {
51 14 50       646 open my $fh, '<', $header_path or die "Cannot read $header_path: $!";
52 14         79 local $/;
53 14         665 $self->{_raw_code} = <$fh>;
54 14         199 close $fh;
55             }
56              
57             # Preprocess and parse
58 14         55 $self->_preprocess($header_path);
59 14         223 $self->_parse_functions();
60 14         156 $self->_parse_constants();
61 14         93 $self->_parse_enums();
62              
63 14         198 return $self;
64             }
65              
66             # Parse header content directly (for testing)
67             sub parse_string {
68 11     11 1 433 my ($self, $code) = @_;
69              
70 11         18 $self->{_raw_code} = $code;
71              
72             # Write to temp file for preprocessing
73 11         118 my ($fh, $tmpfile) = tempfile(SUFFIX => '.h', UNLINK => 1);
74 11         7779 print $fh $code;
75 11         597 close $fh;
76              
77 11         53 $self->_preprocess($tmpfile);
78 11         177 $self->_parse_functions();
79 11         56 $self->_parse_constants();
80 11         62 $self->_parse_enums();
81              
82 11         170 return $self;
83             }
84              
85             # Run C preprocessor
86             sub _preprocess {
87 25     25   65 my ($self, $header_path) = @_;
88              
89 25         66 my $cpp = _find_preprocessor();
90              
91 25         101 my @includes = map { "-I$_" } @{$self->{include}};
  0         0  
  25         235  
92 25         88 my @defines = map { "-D$_=$self->{define}{$_}" } keys %{$self->{define}};
  0         0  
  25         329  
93              
94 25         225 my $cmd = join(' ',
95             $cpp,
96             @includes,
97             @defines,
98             '-x', 'c', # Force C language
99             qq{"$header_path"},
100             '2>/dev/null'
101             );
102              
103 25         515965 $self->{_preprocessed} = `$cmd`;
104              
105 25 50       722 if ($? != 0) {
106 0         0 warn "Preprocessor warning for $header_path (exit code: $?)";
107             # Try to continue with raw code if preprocessing fails
108 0 0       0 $self->{_preprocessed} = $self->{_raw_code} if $self->{_raw_code};
109             }
110              
111 25         647 return $self->{_preprocessed};
112             }
113              
114             # Parse function declarations from preprocessed code
115             sub _parse_functions {
116 25     25   107 my ($self) = @_;
117              
118 25         140 my $code = $self->{_preprocessed};
119 25 50       116 return unless $code;
120              
121             # Remove comments (shouldn't be any after preprocessing, but just in case)
122 25         318 $code =~ s{/\*.*?\*/}{}gs;
123 25         682 $code =~ s{//.*$}{}gm;
124              
125             # Function declaration pattern
126             # Matches: return_type function_name(params);
127             # Handles: const, static, inline, extern, pointers, etc.
128 25         823 my $func_pattern = qr/
129             (?:^|;|\}|\n) # Start of line, after semicolon, or after brace
130             \s*
131             (?:extern\s+|static\s+|inline\s+)* # Optional storage class
132             ( # Capture group 1: return type
133             (?:const\s+)? # Optional const
134             (?:unsigned\s+|signed\s+)? # Optional unsigned\/signed
135             (?:struct\s+|enum\s+|union\s+)? # Optional struct\/enum\/union
136             \w+ # Type name
137             (?:\s+\w+)* # Additional type words (long long, etc.)
138             \s*\** # Optional pointer(s)
139             )
140             \s+
141             (\w+) # Capture group 2: function name
142             \s*
143             \( # Opening paren
144             ([^)]*?) # Capture group 3: parameters
145             \) # Closing paren
146             \s*
147             (?:__attribute__\s*\(\([^)]*\)\))? # Optional GCC attributes
148             \s*; # Semicolon
149             /xm;
150              
151 25         43435 while ($code =~ /$func_pattern/g) {
152 75         796 my ($return_type, $name, $params) = ($1, $2, $3);
153              
154             # Clean up return type
155 75         275 $return_type =~ s/^\s+//;
156 75         205 $return_type =~ s/\s+$//;
157 75         245 $return_type =~ s/\s+/ /g;
158              
159             # Skip if it looks like a macro or type definition
160 75 100       144101 next if $name =~ /^_/; # Skip internal/reserved names
161 56 50       181 next if $return_type =~ /^#/; # Skip preprocessor remnants
162              
163             # Parse parameters
164 56         205 my @parsed_params = $self->_parse_params($params);
165              
166             $self->{functions}{$name} = {
167             name => $name,
168             return_type => $return_type,
169             params => \@parsed_params,
170 59         265 param_types => [ map { $_->{type} } @parsed_params ],
171 56 50       1121 param_names => [ map { $_->{name} } @parsed_params ],
  59         25672  
172             is_variadic => ($params =~ /\.\.\./ ? 1 : 0),
173             };
174             }
175              
176 25         158 return $self->{functions};
177             }
178              
179             # Parse parameter list
180             sub _parse_params {
181 56     56   127 my ($self, $params) = @_;
182              
183 56 50 33     432 return () if !defined $params || $params =~ /^\s*$/;
184 56 100       277 return () if $params =~ /^\s*void\s*$/;
185              
186 30         51 my @result;
187 30         201 my @parts = split /,/, $params;
188              
189 30         54 my $idx = 0;
190 30         92 for my $part (@parts) {
191 59         210 $part =~ s/^\s+//;
192 59         146 $part =~ s/\s+$//;
193              
194             # Skip variadic
195 59 50       120 next if $part eq '...';
196              
197             # Parse "type name" or just "type"
198 59 100       294 if ($part =~ /^(.+?)\s+(\w+)$/) {
    50          
    0          
199 58         615 push @result, { type => $1, name => $2, index => $idx };
200             }
201             elsif ($part =~ /^(.+?)\s*(\*+)\s*(\w+)$/) {
202             # Handle "type *name" or "type * name"
203 1         12 push @result, { type => "$1$2", name => $3, index => $idx };
204             }
205             elsif ($part =~ /^(.+?)$/) {
206             # Just a type, no name
207 0         0 push @result, { type => $1, name => "arg$idx", index => $idx };
208             }
209              
210 59         100 $idx++;
211             }
212              
213 30         142 return @result;
214             }
215              
216             # Parse #define constants from raw (non-preprocessed) code
217             sub _parse_constants {
218 25     25   81 my ($self) = @_;
219              
220 25         217 my $code = $self->{_raw_code};
221 25 50       71 return unless $code;
222              
223             # Match #define NAME value (excluding function-like macros)
224 25         445 while ($code =~ /^\s*#\s*define\s+(\w+)(?!\s*\()[ \t]+(.+?)\s*$/gm) {
225 932         1561 my ($name, $value) = ($1, $2);
226              
227             # Skip common non-value defines
228 932 50       1172 next if $value =~ /^\\/; # Line continuation
229 932 100       4221 next if $name =~ /^_/; # Reserved names
230              
231             # Try to evaluate numeric constants
232 592         787 my $numeric = $self->_try_numeric($value);
233 592 100       667 if (defined $numeric) {
234 256         3537 $self->{constants}{$name} = {
235             name => $name,
236             value => $numeric,
237             raw => $value,
238             };
239             }
240             else {
241             # Store as string constant
242 336         4531 $self->{constants}{$name} = {
243             name => $name,
244             value => $value,
245             raw => $value,
246             string => 1,
247             };
248             }
249             }
250              
251 25         82 return $self->{constants};
252             }
253              
254             # Try to convert a value to a number
255             sub _try_numeric {
256 638     638   824 my ($self, $value) = @_;
257              
258             # Strip trailing C comments (e.g., "3.14159 /* pi */")
259 638         1169 $value =~ s{/\*.*?\*/}{}g;
260 638         1184 $value =~ s{\s+$}{};
261              
262             # Hex
263 638 100       832 if ($value =~ /^0x([0-9a-fA-F]+)(?:U?L{0,2})?$/) {
264 1         7 return hex($1);
265             }
266              
267             # Octal
268 637 50       861 if ($value =~ /^0([0-7]+)(?:U?L{0,2})?$/) {
269 0         0 return oct($1);
270             }
271              
272             # Integer
273 637 100       1306 if ($value =~ /^(-?\d+)(?:U?L{0,2})?$/) {
274 80         186 return $1 + 0;
275             }
276              
277             # Float (with optional trailing whitespace/suffix)
278 557 100       997 if ($value =~ /^(-?\d+\.?\d*(?:[eE][+-]?\d+)?)[fFlL]?\s*$/) {
279 181         502 return $1 + 0.0;
280             }
281              
282             # Simple expression like (1 << 4)
283 376 50       479 if ($value =~ /^\(?\s*(\d+)\s*<<\s*(\d+)\s*\)?$/) {
284 0         0 return $1 << $2;
285             }
286              
287 376         404 return undef;
288             }
289              
290             # Parse enum definitions
291             sub _parse_enums {
292 25     25   58 my ($self) = @_;
293              
294 25         71 my $code = $self->{_raw_code};
295 25 50       63 return unless $code;
296              
297             # Match enum blocks
298 25         214 while ($code =~ /enum\s*(\w*)\s*\{([^}]+)\}/g) {
299 12   100     148 my ($enum_name, $body) = ($1 || 'anonymous', $2);
300              
301 12         31 my $counter = 0;
302 12         32 my @values;
303              
304             # Parse enum values
305 12         87 while ($body =~ /(\w+)(?:\s*=\s*([^,}]+))?/g) {
306 55         104 my ($name, $value) = ($1, $2);
307              
308 55 100       95 if (defined $value) {
309 46         87 my $num = $self->_try_numeric($value);
310 46 100       77 $counter = defined $num ? $num : $counter;
311             }
312              
313 55         176 push @values, { name => $name, value => $counter };
314              
315             # Also add to constants
316 55         241 $self->{constants}{$name} = {
317             name => $name,
318             value => $counter,
319             enum => $enum_name,
320             };
321              
322 55         159 $counter++;
323             }
324              
325 12         133 $self->{enums}{$enum_name} = \@values;
326             }
327              
328 25         51 return $self->{enums};
329             }
330              
331             # Accessors
332 0     0 1 0 sub functions { return %{shift->{functions}} }
  0         0  
333 36     36 1 19329 sub function { my ($self, $name) = @_; return $self->{functions}{$name} }
  36         145  
334 0     0 1 0 sub constants { return %{shift->{constants}} }
  0         0  
335 27     27 1 8494 sub constant { my ($self, $name) = @_; return $self->{constants}{$name} }
  27         139  
336 0     0 1 0 sub enums { return %{shift->{enums}} }
  0         0  
337 0     0 1 0 sub enum { my ($self, $name) = @_; return $self->{enums}{$name} }
  0         0  
338              
339             # Get list of function names
340             sub function_names {
341 5     5 1 33 my ($self) = @_;
342 5         11 return sort keys %{$self->{functions}};
  5         50  
343             }
344              
345             # Get list of constant names
346             sub constant_names {
347 4     4 1 967 my ($self) = @_;
348 4         7 return sort keys %{$self->{constants}};
  4         125  
349             }
350              
351             1;
352              
353             __END__