File Coverage

blib/lib/XS/JIT/Header/Parser.pm
Criterion Covered Total %
statement 137 153 89.5
branch 37 56 66.0
condition 7 9 77.7
subroutine 18 22 81.8
pod 11 11 100.0
total 210 251 83.6


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