File Coverage

blib/lib/CodeGen/Cpppp/Enum.pm
Criterion Covered Total %
statement 247 317 77.9
branch 46 82 56.1
condition 23 56 41.0
subroutine 26 35 74.2
pod 14 19 73.6
total 356 509 69.9


line stmt bran cond sub pod time code
1             package CodeGen::Cpppp::Enum;
2              
3             our $VERSION = '0.005'; # VERSION
4             # ABSTRACT: Helper for enumerations and generating related utility functions
5              
6 1     1   192893 use v5.20;
  1         3  
7 1     1   5 use warnings;
  1         1  
  1         34  
8 1     1   4 use Carp;
  1         1  
  1         67  
9 1     1   417 use experimental 'signatures', 'lexical_subs', 'postderef';
  1         2955  
  1         4  
10 1     1   160 use Scalar::Util 'looks_like_number';
  1         1  
  1         54  
11 1     1   3 use List::Util 'any', 'min', 'max';
  1         1  
  1         139  
12 1   33 1   27 BEGIN { *uniqstr= List::Util->can('uniqstr') // sub { my %seen; grep !$seen{$_}++, @_ } }
  0         0  
  0         0  
13 1     1   420 use CodeGen::Cpppp::CParser;
  1         2  
  1         3622  
14              
15              
16 6     6 1 171512 sub new($class, %attrs) {
  6         8  
  6         14  
  6         5  
17 6         11 my $self= bless {}, $class;
18             # apply num_format first because it affects set_values
19             $self->num_format(delete $attrs{num_format})
20 6 50       34 if exists $attrs{num_format};
21 6         25 $self->$_($attrs{$_}) for keys %attrs;
22 6         13 return $self;
23             }
24              
25              
26 15     15 1 13 sub prefix($self, @val) {
  15         16  
  15         16  
  15         11  
27 15 100       19 if (@val) { $self->{prefix}= $val[0]; return $self }
  4         6  
  4         10  
28 11   50     46 $self->{prefix} // ''
29             }
30              
31 7     7 0 8 sub macro_prefix($self, @val) {
  7         6  
  7         6  
  7         7  
32 7 50       9 if (@val) { $self->{macro_prefix}= $val[0]; return $self }
  0         0  
  0         0  
33 7   33     15 $self->{macro_prefix} // uc($self->prefix);
34             }
35              
36 3     3 0 4 sub symbol_prefix($self, @val) {
  3         3  
  3         3  
  3         3  
37 3 50       5 if (@val) { $self->{symbol_prefix}= $val[0]; return $self }
  0         0  
  0         0  
38 3   33     8 $self->{symbol_prefix} // lc($self->prefix);
39             }
40              
41 1     1 1 1 sub type($self, @val) {
  1         2  
  1         1  
  1         1  
42 1 50       2 if (@val) { $self->{type}= $val[0]; return $self; }
  0         0  
  0         0  
43 1   50     5 $self->{type} // 'int';
44             }
45              
46 20     20 1 31 sub values($self, @val) {
  20         24  
  20         22  
  20         21  
47 20 100       39 return $self->set_values(@val) if @val;
48 11   50     12 @{ $self->{values} // [] }
  11         63  
49             }
50              
51 9     9 0 8 sub set_values($self, @spec) {
  9         9  
  9         12  
  9         7  
52 9         10 my @values;
53 9 100 66     30 for (@spec == 1 && ref $spec[0]? @{$spec[0]} : @spec) {
  7         13  
54 34 100       107 if ('ARRAY' eq ref) {
    100          
55 2         4 push @values, [ @$_ ];
56             } elsif (/^\w+$/) {
57 30         43 push @values, [ $_ ];
58             } else {
59 2 50       11 defined $values[-1] or croak "Got an enum value '$_' before a name";
60 2 50       4 defined $values[-1][1] and croak "'$_' is not a valid enum name";
61 2         5 $values[-1][1]= $_;
62             }
63             }
64             # Fill in missing values with next sequential integer
65 9   100     29 my $prev= $values[0][1] //= 0;
66 9         18 for (@values[1..$#values]) {
67 23 100       37 if (!defined $_->[1]) {
68 22         33 my ($base, $ofs, $fmt)= $self->_parse_value_expr($prev);
69 22         55 $_->[1]= sprintf $fmt, $ofs+1;
70             }
71 23         28 $prev= $_->[1];
72             }
73 9         21 $self->{values}= \@values;
74 9         12 $self->{_analysis}= undef;
75 9         20 $self;
76             }
77              
78 3     3 1 4 sub value_table_var($self, @val) {
  3         3  
  3         3  
  3         3  
79 3 50       4 if (@val) {
80 0         0 $self->{value_table_var}= $val[0];
81 0         0 return $self;
82             }
83 3   33     9 $self->{value_table_var} // $self->symbol_prefix . 'value_table';
84             }
85              
86 4     4 1 4 sub indent($self, @val) {
  4         22  
  4         6  
  4         4  
87 4 50       7 if (@val) {
88 0         0 $self->{indent}= $val[0];
89 0         0 return $self;
90             }
91 4   50     13 $self->{indent} // ' ';
92             }
93              
94             sub _current_indent {
95 4   33 4   10 $CodeGen::Cpppp::INDENT // shift->indent;
96             }
97              
98 0     0 1 0 sub num_format($self, @val) {
  0         0  
  0         0  
  0         0  
99 0 0       0 if (@val) {
100 0         0 $self->{num_format}= $val[0];
101 0         0 $self->{_analysis}= undef;
102 0         0 return $self;
103             }
104 0   0     0 $self->{num_format} // '%d';
105             }
106              
107 4     4 0 5 sub max_waste_factor($self, @val) {
  4         4  
  4         4  
  4         2  
108 4 50       7 if (@val) {
109 0         0 $self->{max_waste_factor}= $val[0];
110 0         0 $self->{_analysis}= undef;
111 0         0 return $self;
112             }
113 4   50     19 $self->{max_waste_factor} // 2;
114             }
115              
116              
117             our %_algorithm= map +( $_ => 1 ), qw( bsearch hashtable switch );
118 0     0 1 0 sub algorithm($self, @val) {
  0         0  
  0         0  
  0         0  
119 0 0       0 if (@val) {
120 0 0 0     0 !defined $val[0] or $_algorithm{$val[0]}
121             or croak "Unknown parse_design '$val[0]', expected one of ".join(', ', keys %_algorithm);
122 0         0 $self->{algorithm}= $val[0];
123 0         0 return $self;
124             }
125             $self->{algorithm}
126 0         0 }
127              
128 30     30   30 sub _parse_value_expr($self, $val) {
  30         25  
  30         25  
  30         29  
129             # Make the common case fast
130 30 100       114 return '', +$val, '%d'
131             if $val =~ /^[-+]?(?:0|[1-9][0-9]*)\Z/;
132             # else need to parse the expression
133 5         17 my @tokens= CodeGen::Cpppp::CParser->tokenize($val);
134 5         12 my $type_pattern= join '', map $_->type, @tokens;
135             # Recognize patterns where a +N occurs at the end of the expression
136             # Else, the whole value is the expression and will get '+N' appended.
137 5 50 0     19 return $val, 0, "($val+".($self->{num_format}//'%d').")"
138             unless $type_pattern =~ /(^|[-+])integer\W*$/;
139 5         9 my $context= $1;
140             # walk backward to last 'integer' token
141 5         6 my $i= $#tokens;
142 5         8 $i-- while $tokens[$i]->type ne 'integer';
143             # could be start of string, -N, +N, EXPR-N, EXPR+N, or EXPR OP -N
144 5         6 my $fmt_str= $val;
145 5         11 my ($pos, $pos2)= ($tokens[$i]->src_pos, $tokens[$i]->src_pos+$tokens[$i]->src_len);
146 5         8 my $n= $tokens[$i]->value;
147             # If start of string or preceeded by '+', nothing to do.
148             # If preceeded by '-', need to convert that to '+' in format string
149 5 100       7 if ($context eq '-') {
150 1         2 $n= -$n;
151 1         3 $pos= $tokens[$i-1]->src_pos + 1;
152 1         22 substr($fmt_str, $tokens[$i-1]->src_pos, 1, '+');
153             }
154 5         10 my $num_str= substr($val, $tokens[$i]->src_pos, $tokens[$i]->src_len);
155             my $notation= $self->{num_format}
156 5 50 33     20 // $num_str =~ /^-?0x[0-9A-F]+$/? 'X'
    50          
    50          
157             : $num_str =~ /^-?0x[0-9a-f]+$/? 'x'
158             : $num_str =~ /^-?0[0-9]+/? 'o'
159             : 'd';
160 5         16 substr($fmt_str, $pos, $pos2-$pos, '%'.($pos2-$pos).$notation);
161             # The "base" is everying to the left of the number minus the number of "("
162             # to match the number of ")" to the right of the number
163 5         12 my $rparen= grep $_->type eq ')', @tokens[$i..$#tokens];
164 5   66     8 shift @tokens while $tokens[0]->type eq '(' && $rparen--;
165 5         7 my $base= substr($val, $tokens[0]->src_pos, $pos-$tokens[0]->src_pos);
166 5         20 return ($base, $n, $fmt_str);
167             }
168              
169              
170 0     0 1 0 sub is_symbolic($self) {
  0         0  
  0         0  
171 0         0 $self->_analysis->{base_expr} ne '';
172             }
173              
174 0     0 1 0 sub is_sequential($self) {
  0         0  
  0         0  
175             $self->_analysis->{is_seq}
176 0         0 }
177              
178 0     0 0 0 sub is_nearly_sequential($self) {
  0         0  
  0         0  
179             $self->_analysis->{is_nearly_seq}
180 0         0 }
181              
182 2     2   2 sub _analysis($self) {
  2         3  
  2         2  
183 2   33     6 $self->{_analysis} //= do {
184 2         3 my @vals= map +[ $_->[0], $self->_parse_value_expr($_->[1]) ], $self->values;
185 2         4 my $base_expr= $vals[0][1];
186 2         5 my %seen_ofs= ( $vals[0][2] => 1 );
187 2         14 for (@vals[1..$#vals]) {
188             # Can't be sequential unless they share a symbolic base expression
189 6 50       12 $base_expr= undef, last
190             unless $_->[1] eq $base_expr;
191 6         9 $seen_ofs{$_->[2]}++;
192             }
193 2         5 my %info= (
194             vals => \@vals
195             );
196 2 50       4 if (defined $base_expr) {
197             # Find the min/max
198 2         18 my ($min, $max)= (min(keys %seen_ofs), max(keys %seen_ofs));
199             # Is it sequential?
200 2         3 my ($is_seq, $is_nearly_seq, $gap);
201             # don't iterate unless the range is reasonable
202 2 50       6 if (($max - $min - @vals) <= $self->max_waste_factor * @vals) {
203 2         2 $gap= 0;
204 2         6 for ($min .. $max) {
205 8 50       14 $gap++ unless $seen_ofs{$_};
206             }
207 2         2 $is_seq= $gap == 0;
208 2         3 $is_nearly_seq= $gap <= $self->max_waste_factor * ($max-$min+1-$gap);
209             }
210 2         4 $info{is_seq}= $is_seq;
211 2         7 $info{is_nearly_seq}= $is_nearly_seq;
212 2         5 $info{gap}= $gap;
213 2         3 $info{min}= $min;
214 2         3 $info{max}= $max;
215 2         4 $info{base_expr}= $base_expr;
216             }
217 2         6 \%info
218             };
219             }
220              
221              
222 0     0 1 0 sub generate_declaration($self, %options) {
  0         0  
  0         0  
  0         0  
223 0         0 return join "\n", $self->_generate_declaration_macros(\%options);
224             }
225              
226 1     1   5 sub _generate_declaration_macros($self, $options) {
  1         2  
  1         1  
  1         1  
227 1         3 my @vals= $self->values;
228 1         10 my $name_width= max map length($_->[0]), @vals;
229 1         3 my $prefix= $self->macro_prefix;
230 1         3 my $fmt= "#define $prefix%-${name_width}s %s";
231 1         9 return map sprintf($fmt, $_->[0], $_->[1]), @vals;
232             }
233              
234              
235 0     0 1 0 sub generate_static_tables($self, %options) {
  0         0  
  0         0  
  0         0  
236 0         0 return join "\n", _generate_enum_table($self, \%options);
237             }
238              
239 1     1   6 sub _generate_enum_table($self, $options) {
  1         1  
  1         2  
  1         1  
240 1         2 my $prefix= $self->prefix;
241 1         3 my @names= map $prefix . $_->[0], $self->values;
242 1         5 my $name_width= max map length, @names;
243 1         4 my $indent= $self->_current_indent;
244 1         3 my $fmt= $indent.$indent.'{ "%s",%*s %s },';
245 1         3 my @code= (
246             "const struct { const char *name; const ".$self->type." value; }",
247             $indent . $self->value_table_var . "[] = {",
248             (map sprintf($fmt, $_, $name_width-length, '', $_), @names),
249             $indent . '};'
250             );
251 1         3 substr($code[-2], -1, 1, ''); # remove trailing comma
252 1         22 return @code;
253             }
254              
255              
256 0     0 1 0 sub generate_lookup_by_value($self, %options) {
  0         0  
  0         0  
  0         0  
257 0         0 return join "\n", $self->_generate_lookup_by_value_switch(\%options);
258             }
259              
260 1     1   6 sub _generate_lookup_by_value_switch($self, $options) {
  1         2  
  1         1  
  1         1  
261 1         3 my @vals= $self->values;
262 1         6 my $name_width= max map length($_->[0]), @vals;
263 1         3 my $info= $self->_analysis;
264 1         2 my $val_variable= 'value';
265 1         7 my $prefix= $self->macro_prefix;
266 1         2 my $enum_table= $self->value_table_var;
267             # Generate a switch() table to look them up
268 1         4 my @code= "switch ($val_variable) {";
269 1         2 my $fmt= "case $prefix%s:%*s return ${enum_table}[%d].name;";
270 1         3 for (0..$#vals) {
271 4         12 push @code, sprintf($fmt, $vals[$_][0], $name_width - length($vals[$_][0]), '', $_);
272             }
273 1         2 push @code, 'default: return NULL;', '}';
274 1         20 return @code;
275             }
276              
277              
278 0     0 1 0 sub generate_lookup_by_name($self, %options) {
  0         0  
  0         0  
  0         0  
279 0         0 return join "\n", $self->_generate_lookup_by_name_switch(\%options);
280             }
281              
282 1     1   5 sub _generate_lookup_by_name_switch($self, $options) {
  1         2  
  1         2  
  1         2  
283 1         2 my @vals= $self->values;
284 1         3 my $info= $self->_analysis;
285 1         2 my $caseless= $options->{caseless};
286 1         2 my $prefixless= $options->{prefixless};
287 1         3 my $prefixlen= length($self->macro_prefix);
288 1         2 my $indent= $self->_current_indent;
289 1   50     5 my $len_var= $options->{len_var} // 'len';
290 1   50     3 my $str_ptr= $options->{str_ptr} // 'str';
291 1         2 my $enum_table= $self->value_table_var;
292 1 50       4 my $strcmp= $caseless? "strcasecmp" : "strcmp";
293 1 0       3 my $idx_type= @vals <= 0x7F? 'int8_t'
    0          
    50          
294             : @vals <= 0x7FFF? 'int16_t'
295             : @vals <= 0x7FFFFFFF? 'int32_t'
296             : 'int64_t';
297 1         1 my @search_set;
298 1         3 for (0..$#vals) {
299 4         5 push @search_set, [ $self->macro_prefix . $vals[$_][0], $_ ];
300 4 50       7 push @search_set, [ $vals[$_][0], -$_ ] if $prefixless;
301             }
302 1         2 my %by_len;
303 1         2 for (@search_set) {
304 4         5 push @{ $by_len{length $_->[0]} }, $_;
  4         7  
305             }
306 1         4 my $longest= max(keys %by_len);
307 1         4 my @code= (
308             "$idx_type test_el= 0;",
309             ("char str_buf[$longest+1];")x!!$caseless,
310             "switch ($len_var) {",
311             );
312             # Generate one binary decision tree for each string length
313 1         5 for (sort { $a <=> $b } keys %by_len) {
  1         4  
314 2         3 my %pivot_pos;
315 2 50       18 my @split_expr= $self->_binary_split($by_len{$_}, $caseless, $caseless? 'str_buf' : $str_ptr, \%pivot_pos);
316             push @code,
317             "case $_:",
318             ($caseless? (
319             map "${indent}str_buf[$_]= tolower(${str_ptr}[$_]);",
320 2 50       14 sort { $a <=> $b } keys %pivot_pos
  0         0  
321             ) : ()),
322             (map "$indent$_", @split_expr),
323             "${indent}break;",
324             }
325 1         3 push @code,
326             "default:",
327             "${indent}return false;",
328             "}";
329             # If allowing prefixless match, some test_el will be negative, meaning to
330             # test str+prefixlen
331 1 50       2 if ($prefixless) {
332 0         0 push @code,
333             "if (test_el < 0) {",
334             "${indent}if ($strcmp($str_ptr, ${enum_table}[-test_el].name + $prefixlen) == 0) {",
335             "${indent}${indent}if (value_out) *value_out= ${enum_table}[-test_el].value;",
336             "${indent}${indent}return true;",
337             "${indent}}",
338             "${indent}return false;",
339             "}";
340             }
341 1         5 push @code,
342             "if ($strcmp($str_ptr, ${enum_table}[test_el].name) == 0) {",
343             "${indent}if (value_out) *value_out= ${enum_table}[test_el].value;",
344             "${indent}return true;",
345             "}",
346             "return false;";
347 1         24 return @code;
348             }
349              
350 6     6   6 sub _binary_split($self, $vals, $caseless, $str_var, $pivot_pos) {
  6         9  
  6         6  
  6         5  
  6         26  
  6         5  
  6         4  
351             # Stop at length 1
352 6 100       18 return qq{test_el= $vals->[0][1];}
353             if @$vals == 1;
354             # Find a character comparison that splits the list roughly in half.
355 2         3 my $goal= .5 * scalar @$vals;
356             # Test every possible character and keep track of the best.
357 2         3 my ($best_i, $best_ch, $best_less);
358 2         6 for (my $i= 0; $i < length $vals->[0][0]; ++$i) {
359 14 50       13 if (!$caseless) {
360 14         40 for my $ch (uniqstr map substr($_->[0], $i, 1), @$vals) {
361 17         32 my @less= grep substr($_->[0], $i, 1) lt $ch, @$vals;
362 17 100 100     54 ($best_i, $best_ch, $best_less)= ($i, $ch, \@less)
363             if !defined $best_i || abs($goal - @less) < abs($goal - @$best_less);
364             }
365             } else {
366 0         0 for my $ch (uniqstr map lc substr($_->[0], $i, 1), @$vals) {
367 0         0 my @less= grep +(lc(substr($_->[0], $i, 1)) lt $ch), @$vals;
368 0 0 0     0 ($best_i, $best_ch, $best_less)= ($i, $ch, \@less)
369             if !defined $best_i || abs($goal - @less) < abs($goal - @$best_less);
370             }
371             }
372             }
373 2         4 $pivot_pos->{$best_i}++; # inform caller of which chars were used
374             # Binary split the things less than the pivot character
375 2         13 my @less_src= $self->_binary_split($best_less, $caseless, $str_var, $pivot_pos);
376             # Binary split the things greater-or-equal to the pivot character
377 2         11 my %less= map +($_->[0] => 1), @$best_less;
378 2         7 my @ge_src= $self->_binary_split([ grep !$less{$_->[0]}, @$vals ], $caseless, $str_var, $pivot_pos);
379 2         4 my $indent= $self->_current_indent;
380             return (
381 2 100       16 "if (${str_var}[$best_i] < '$best_ch') {",
382             (map $indent.$_, @less_src),
383             (@ge_src > 1
384             # combine "else { if"
385             ? ( '} else '.$ge_src[0], @ge_src[1..$#ge_src] )
386             # else { statement }
387             : ( '} else {', (map $indent.$_, @ge_src), '}' )
388             )
389             );
390             }
391              
392             1;
393              
394             __END__