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   101647 use strict;
  4         6  
  4         125  
4 4     4   14 use warnings;
  4         4  
  4         185  
5 4     4   18 use File::Temp qw(tempfile);
  4         6  
  4         226  
6 4     4   25 use File::Spec;
  4         12  
  4         11755  
7              
8             our $VERSION = '0.19';
9              
10             # Find a working C preprocessor
11             sub _find_preprocessor {
12 25     25   121 my @candidates = qw(clang cpp gcc);
13 25         430 my $devnull = File::Spec->devnull();
14              
15 25         91 for my $cmd (@candidates) {
16 50         320432 my $output = `$cmd --version 2>$devnull`;
17 50 100       1531 if ($? == 0) {
18 25         968 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 181815 my ($class, %opts) = @_;
28              
29             my $self = bless {
30             include => $opts{include} || [],
31             define => $opts{define} || {},
32 28   100     383 functions => {},
      100        
33             constants => {},
34             typedefs => {},
35             enums => {},
36             structs => {},
37             _raw_code => '',
38             _preprocessed => '',
39             }, $class;
40              
41 28         94 return $self;
42             }
43              
44             # Parse a header file
45             sub parse_file {
46 14     14 1 36 my ($self, $header_path) = @_;
47              
48 14 50       33 die "Header file required" unless defined $header_path;
49              
50             # Read raw header for constant extraction
51 14 50       154 if (-f $header_path) {
52 14 50       649 open my $fh, '<', $header_path or die "Cannot read $header_path: $!";
53 14         97 local $/;
54 14         800 $self->{_raw_code} = <$fh>;
55 14         245 close $fh;
56             }
57              
58             # Preprocess and parse
59 14         72 $self->_preprocess($header_path);
60 14         219 $self->_parse_functions();
61 14         152 $self->_parse_constants();
62 14         90 $self->_parse_enums();
63              
64 14         170 return $self;
65             }
66              
67             # Parse header content directly (for testing)
68             sub parse_string {
69 11     11 1 379 my ($self, $code) = @_;
70              
71 11         22 $self->{_raw_code} = $code;
72              
73             # Write to temp file for preprocessing
74 11         168 my ($fh, $tmpfile) = tempfile(SUFFIX => '.h', UNLINK => 1);
75 11         9687 print $fh $code;
76 11         623 close $fh;
77              
78 11         50 $self->_preprocess($tmpfile);
79 11         204 $self->_parse_functions();
80 11         68 $self->_parse_constants();
81 11         87 $self->_parse_enums();
82              
83 11         157 return $self;
84             }
85              
86             # Run C preprocessor
87             sub _preprocess {
88 25     25   63 my ($self, $header_path) = @_;
89              
90 25         84 my $cpp = _find_preprocessor();
91              
92 25         126 my @includes = map { "-I$_" } @{$self->{include}};
  0         0  
  25         187  
93 25         127 my @defines = map { "-D$_=$self->{define}{$_}" } keys %{$self->{define}};
  0         0  
  25         352  
94              
95 25         226 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         508069 $self->{_preprocessed} = `$cmd`;
105              
106 25 50       821 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         626 return $self->{_preprocessed};
113             }
114              
115             # Parse function declarations from preprocessed code
116             sub _parse_functions {
117 25     25   108 my ($self) = @_;
118              
119 25         177 my $code = $self->{_preprocessed};
120 25 50       264 return unless $code;
121              
122             # Remove comments (shouldn't be any after preprocessing, but just in case)
123 25         307 $code =~ s{/\*.*?\*/}{}gs;
124 25         742 $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         974 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         43802 while ($code =~ /$func_pattern/g) {
153 75         777 my ($return_type, $name, $params) = ($1, $2, $3);
154              
155             # Clean up return type
156 75         245 $return_type =~ s/^\s+//;
157 75         236 $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       144105 next if $name =~ /^_/; # Skip internal/reserved names
162 56 50       165 next if $return_type =~ /^#/; # Skip preprocessor remnants
163              
164             # Parse parameters
165 56         224 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         281 param_types => [ map { $_->{type} } @parsed_params ],
172 56 50       933 param_names => [ map { $_->{name} } @parsed_params ],
  59         26156  
173             is_variadic => ($params =~ /\.\.\./ ? 1 : 0),
174             };
175             }
176              
177 25         161 return $self->{functions};
178             }
179              
180             # Parse parameter list
181             sub _parse_params {
182 56     56   142 my ($self, $params) = @_;
183              
184 56 50 33     420 return () if !defined $params || $params =~ /^\s*$/;
185 56 100       259 return () if $params =~ /^\s*void\s*$/;
186              
187 30         56 my @result;
188 30         186 my @parts = split /,/, $params;
189              
190 30         48 my $idx = 0;
191 30         89 for my $part (@parts) {
192 59         168 $part =~ s/^\s+//;
193 59         142 $part =~ s/\s+$//;
194              
195             # Skip variadic
196 59 50       119 next if $part eq '...';
197              
198             # Parse "type name" or just "type"
199 59 100       304 if ($part =~ /^(.+?)\s+(\w+)$/) {
    50          
    0          
200 58         546 push @result, { type => $1, name => $2, index => $idx };
201             }
202             elsif ($part =~ /^(.+?)\s*(\*+)\s*(\w+)$/) {
203             # Handle "type *name" or "type * name"
204 1         12 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         92 $idx++;
212             }
213              
214 30         120 return @result;
215             }
216              
217             # Parse #define constants from raw (non-preprocessed) code
218             sub _parse_constants {
219 25     25   64 my ($self) = @_;
220              
221 25         126 my $code = $self->{_raw_code};
222 25 50       91 return unless $code;
223              
224             # Match #define NAME value (excluding function-like macros)
225 25         500 while ($code =~ /^\s*#\s*define\s+(\w+)(?!\s*\()[ \t]+(.+?)\s*$/gm) {
226 932         1797 my ($name, $value) = ($1, $2);
227              
228             # Skip common non-value defines
229 932 50       1217 next if $value =~ /^\\/; # Line continuation
230 932 100       4248 next if $name =~ /^_/; # Reserved names
231              
232             # Try to evaluate numeric constants
233 592         864 my $numeric = $self->_try_numeric($value);
234 592 100       745 if (defined $numeric) {
235 256         3542 $self->{constants}{$name} = {
236             name => $name,
237             value => $numeric,
238             raw => $value,
239             };
240             }
241             else {
242             # Store as string constant
243 336         5184 $self->{constants}{$name} = {
244             name => $name,
245             value => $value,
246             raw => $value,
247             string => 1,
248             };
249             }
250             }
251              
252 25         76 return $self->{constants};
253             }
254              
255             # Try to convert a value to a number
256             sub _try_numeric {
257 638     638   789 my ($self, $value) = @_;
258              
259             # Strip trailing C comments (e.g., "3.14159 /* pi */")
260 638         1373 $value =~ s{/\*.*?\*/}{}g;
261 638         1363 $value =~ s{\s+$}{};
262              
263             # Hex
264 638 100       1041 if ($value =~ /^0x([0-9a-fA-F]+)(?:U?L{0,2})?$/) {
265 1         4 return hex($1);
266             }
267              
268             # Octal
269 637 50       878 if ($value =~ /^0([0-7]+)(?:U?L{0,2})?$/) {
270 0         0 return oct($1);
271             }
272              
273             # Integer
274 637 100       1420 if ($value =~ /^(-?\d+)(?:U?L{0,2})?$/) {
275 80         177 return $1 + 0;
276             }
277              
278             # Float (with optional trailing whitespace/suffix)
279 557 100       1195 if ($value =~ /^(-?\d+\.?\d*(?:[eE][+-]?\d+)?)[fFlL]?\s*$/) {
280 181         528 return $1 + 0.0;
281             }
282              
283             # Simple expression like (1 << 4)
284 376 50       567 if ($value =~ /^\(?\s*(\d+)\s*<<\s*(\d+)\s*\)?$/) {
285 0         0 return $1 << $2;
286             }
287              
288 376         447 return undef;
289             }
290              
291             # Parse enum definitions
292             sub _parse_enums {
293 25     25   57 my ($self) = @_;
294              
295 25         54 my $code = $self->{_raw_code};
296 25 50       84 return unless $code;
297              
298             # Match enum blocks
299 25         212 while ($code =~ /enum\s*(\w*)\s*\{([^}]+)\}/g) {
300 12   100     161 my ($enum_name, $body) = ($1 || 'anonymous', $2);
301              
302 12         31 my $counter = 0;
303 12         34 my @values;
304              
305             # Parse enum values
306 12         90 while ($body =~ /(\w+)(?:\s*=\s*([^,}]+))?/g) {
307 55         117 my ($name, $value) = ($1, $2);
308              
309 55 100       101 if (defined $value) {
310 46         81 my $num = $self->_try_numeric($value);
311 46 100       79 $counter = defined $num ? $num : $counter;
312             }
313              
314 55         149 push @values, { name => $name, value => $counter };
315              
316             # Also add to constants
317 55         302 $self->{constants}{$name} = {
318             name => $name,
319             value => $counter,
320             enum => $enum_name,
321             };
322              
323 55         174 $counter++;
324             }
325              
326 12         168 $self->{enums}{$enum_name} = \@values;
327             }
328              
329 25         58 return $self->{enums};
330             }
331              
332             # Accessors
333 0     0 1 0 sub functions { return %{shift->{functions}} }
  0         0  
334 36     36 1 21679 sub function { my ($self, $name) = @_; return $self->{functions}{$name} }
  36         190  
335 0     0 1 0 sub constants { return %{shift->{constants}} }
  0         0  
336 27     27 1 9795 sub constant { my ($self, $name) = @_; return $self->{constants}{$name} }
  27         143  
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 38 my ($self) = @_;
343 5         15 return sort keys %{$self->{functions}};
  5         58  
344             }
345              
346             # Get list of constant names
347             sub constant_names {
348 4     4 1 1027 my ($self) = @_;
349 4         10 return sort keys %{$self->{constants}};
  4         128  
350             }
351              
352             1;
353              
354             __END__