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   139372 use strict;
  4         7  
  4         153  
4 4     4   18 use warnings;
  4         7  
  4         259  
5 4     4   26 use File::Temp qw(tempfile);
  4         7  
  4         286  
6 4     4   23 use File::Spec;
  4         14  
  4         18169  
7              
8             our $VERSION = '0.22';
9              
10             # Find a working C preprocessor
11             sub _find_preprocessor {
12 25     25   111 my @candidates = qw(clang cpp gcc);
13 25         536 my $devnull = File::Spec->devnull();
14              
15 25         131 for my $cmd (@candidates) {
16 50         437214 my $output = `$cmd --version 2>$devnull`;
17 50 100       2120 if ($? == 0) {
18 25         1974 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 209189 my ($class, %opts) = @_;
28              
29             my $self = bless {
30             include => $opts{include} || [],
31             define => $opts{define} || {},
32 28   100     487 functions => {},
      100        
33             constants => {},
34             typedefs => {},
35             enums => {},
36             structs => {},
37             _raw_code => '',
38             _preprocessed => '',
39             }, $class;
40              
41 28         119 return $self;
42             }
43              
44             # Parse a header file
45             sub parse_file {
46 14     14 1 49 my ($self, $header_path) = @_;
47              
48 14 50       45 die "Header file required" unless defined $header_path;
49              
50             # Read raw header for constant extraction
51 14 50       147 if (-f $header_path) {
52 14 50       794 open my $fh, '<', $header_path or die "Cannot read $header_path: $!";
53 14         117 local $/;
54 14         956 $self->{_raw_code} = <$fh>;
55 14         265 close $fh;
56             }
57              
58             # Preprocess and parse
59 14         69 $self->_preprocess($header_path);
60 14         307 $self->_parse_functions();
61 14         269 $self->_parse_constants();
62 14         103 $self->_parse_enums();
63              
64 14         179 return $self;
65             }
66              
67             # Parse header content directly (for testing)
68             sub parse_string {
69 11     11 1 401 my ($self, $code) = @_;
70              
71 11         23 $self->{_raw_code} = $code;
72              
73             # Write to temp file for preprocessing
74 11         195 my ($fh, $tmpfile) = tempfile(SUFFIX => '.h', UNLINK => 1);
75 11         8874 print $fh $code;
76 11         573 close $fh;
77              
78 11         66 $self->_preprocess($tmpfile);
79 11         209 $self->_parse_functions();
80 11         85 $self->_parse_constants();
81 11         71 $self->_parse_enums();
82              
83 11         182 return $self;
84             }
85              
86             # Run C preprocessor
87             sub _preprocess {
88 25     25   91 my ($self, $header_path) = @_;
89              
90 25         114 my $cpp = _find_preprocessor();
91              
92 25         115 my @includes = map { "-I$_" } @{$self->{include}};
  0         0  
  25         451  
93 25         114 my @defines = map { "-D$_=$self->{define}{$_}" } keys %{$self->{define}};
  0         0  
  25         2124  
94              
95 25         1140 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         801029 $self->{_preprocessed} = `$cmd`;
105              
106 25 50       719 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         733 return $self->{_preprocessed};
113             }
114              
115             # Parse function declarations from preprocessed code
116             sub _parse_functions {
117 25     25   118 my ($self) = @_;
118              
119 25         149 my $code = $self->{_preprocessed};
120 25 50       205 return unless $code;
121              
122             # Remove comments (shouldn't be any after preprocessing, but just in case)
123 25         338 $code =~ s{/\*.*?\*/}{}gs;
124 25         758 $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         1062 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         64731 while ($code =~ /$func_pattern/g) {
153 75         1002 my ($return_type, $name, $params) = ($1, $2, $3);
154              
155             # Clean up return type
156 75         313 $return_type =~ s/^\s+//;
157 75         277 $return_type =~ s/\s+$//;
158 75         246 $return_type =~ s/\s+/ /g;
159              
160             # Skip if it looks like a macro or type definition
161 75 100       195977 next if $name =~ /^_/; # Skip internal/reserved names
162 56 50       210 next if $return_type =~ /^#/; # Skip preprocessor remnants
163              
164             # Parse parameters
165 56         261 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         300 param_types => [ map { $_->{type} } @parsed_params ],
172 56 50       1320 param_names => [ map { $_->{name} } @parsed_params ],
  59         36543  
173             is_variadic => ($params =~ /\.\.\./ ? 1 : 0),
174             };
175             }
176              
177 25         185 return $self->{functions};
178             }
179              
180             # Parse parameter list
181             sub _parse_params {
182 56     56   179 my ($self, $params) = @_;
183              
184 56 50 33     544 return () if !defined $params || $params =~ /^\s*$/;
185 56 100       344 return () if $params =~ /^\s*void\s*$/;
186              
187 30         46 my @result;
188 30         181 my @parts = split /,/, $params;
189              
190 30         65 my $idx = 0;
191 30         100 for my $part (@parts) {
192 59         251 $part =~ s/^\s+//;
193 59         237 $part =~ s/\s+$//;
194              
195             # Skip variadic
196 59 50       132 next if $part eq '...';
197              
198             # Parse "type name" or just "type"
199 59 100       343 if ($part =~ /^(.+?)\s+(\w+)$/) {
    50          
    0          
200 58         716 push @result, { type => $1, name => $2, index => $idx };
201             }
202             elsif ($part =~ /^(.+?)\s*(\*+)\s*(\w+)$/) {
203             # Handle "type *name" or "type * name"
204 1         10 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         157 $idx++;
212             }
213              
214 30         183 return @result;
215             }
216              
217             # Parse #define constants from raw (non-preprocessed) code
218             sub _parse_constants {
219 25     25   75 my ($self) = @_;
220              
221 25         141 my $code = $self->{_raw_code};
222 25 50       93 return unless $code;
223              
224             # Match #define NAME value (excluding function-like macros)
225 25         488 while ($code =~ /^\s*#\s*define\s+(\w+)(?!\s*\()[ \t]+(.+?)\s*$/gm) {
226 932         3654 my ($name, $value) = ($1, $2);
227              
228             # Skip common non-value defines
229 932 50       1734 next if $value =~ /^\\/; # Line continuation
230 932 100       6065 next if $name =~ /^_/; # Reserved names
231              
232             # Try to evaluate numeric constants
233 592         1164 my $numeric = $self->_try_numeric($value);
234 592 100       1024 if (defined $numeric) {
235 256         5586 $self->{constants}{$name} = {
236             name => $name,
237             value => $numeric,
238             raw => $value,
239             };
240             }
241             else {
242             # Store as string constant
243 336         6870 $self->{constants}{$name} = {
244             name => $name,
245             value => $value,
246             raw => $value,
247             string => 1,
248             };
249             }
250             }
251              
252 25         138 return $self->{constants};
253             }
254              
255             # Try to convert a value to a number
256             sub _try_numeric {
257 638     638   1149 my ($self, $value) = @_;
258              
259             # Strip trailing C comments (e.g., "3.14159 /* pi */")
260 638         2011 $value =~ s{/\*.*?\*/}{}g;
261 638         1977 $value =~ s{\s+$}{};
262              
263             # Hex
264 638 100       1394 if ($value =~ /^0x([0-9a-fA-F]+)(?:U?L{0,2})?$/) {
265 1         12 return hex($1);
266             }
267              
268             # Octal
269 637 50       1174 if ($value =~ /^0([0-7]+)(?:U?L{0,2})?$/) {
270 0         0 return oct($1);
271             }
272              
273             # Integer
274 637 100       2021 if ($value =~ /^(-?\d+)(?:U?L{0,2})?$/) {
275 80         269 return $1 + 0;
276             }
277              
278             # Float (with optional trailing whitespace/suffix)
279 557 100       1545 if ($value =~ /^(-?\d+\.?\d*(?:[eE][+-]?\d+)?)[fFlL]?\s*$/) {
280 181         806 return $1 + 0.0;
281             }
282              
283             # Simple expression like (1 << 4)
284 376 50       661 if ($value =~ /^\(?\s*(\d+)\s*<<\s*(\d+)\s*\)?$/) {
285 0         0 return $1 << $2;
286             }
287              
288 376         628 return undef;
289             }
290              
291             # Parse enum definitions
292             sub _parse_enums {
293 25     25   65 my ($self) = @_;
294              
295 25         64 my $code = $self->{_raw_code};
296 25 50       99 return unless $code;
297              
298             # Match enum blocks
299 25         229 while ($code =~ /enum\s*(\w*)\s*\{([^}]+)\}/g) {
300 12   100     190 my ($enum_name, $body) = ($1 || 'anonymous', $2);
301              
302 12         63 my $counter = 0;
303 12         36 my @values;
304              
305             # Parse enum values
306 12         96 while ($body =~ /(\w+)(?:\s*=\s*([^,}]+))?/g) {
307 55         153 my ($name, $value) = ($1, $2);
308              
309 55 100       136 if (defined $value) {
310 46         114 my $num = $self->_try_numeric($value);
311 46 100       105 $counter = defined $num ? $num : $counter;
312             }
313              
314 55         263 push @values, { name => $name, value => $counter };
315              
316             # Also add to constants
317 55         372 $self->{constants}{$name} = {
318             name => $name,
319             value => $counter,
320             enum => $enum_name,
321             };
322              
323 55         215 $counter++;
324             }
325              
326 12         165 $self->{enums}{$enum_name} = \@values;
327             }
328              
329 25         60 return $self->{enums};
330             }
331              
332             # Accessors
333 0     0 1 0 sub functions { return %{shift->{functions}} }
  0         0  
334 36     36 1 25600 sub function { my ($self, $name) = @_; return $self->{functions}{$name} }
  36         137  
335 0     0 1 0 sub constants { return %{shift->{constants}} }
  0         0  
336 27     27 1 15473 sub constant { my ($self, $name) = @_; return $self->{constants}{$name} }
  27         215  
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         20 return sort keys %{$self->{functions}};
  5         79  
344             }
345              
346             # Get list of constant names
347             sub constant_names {
348 4     4 1 1008 my ($self) = @_;
349 4         14 return sort keys %{$self->{constants}};
  4         135  
350             }
351              
352             1;
353              
354             __END__