File Coverage

blib/lib/Devel/Optic/Lens/Perlish/Interpreter.pm
Criterion Covered Total %
statement 115 126 91.2
branch 63 74 85.1
condition 17 21 80.9
subroutine 15 15 100.0
pod 0 1 0.0
total 210 237 88.6


line stmt bran cond sub pod time code
1             package Devel::Optic::Lens::Perlish::Interpreter;
2             $Devel::Optic::Lens::Perlish::Interpreter::VERSION = '0.013';
3             # ABSTRACT: Basic recursive interpreter for Perlish lens
4              
5 3     3   18 use strict;
  3         5  
  3         72  
6 3     3   12 use warnings;
  3         11  
  3         72  
7              
8 3     3   20 use Exporter qw(import);
  3         5  
  3         131  
9             our @EXPORT_OK = qw(run);
10              
11 3     3   17 use Carp qw(croak);
  3         5  
  3         182  
12             our @CARP_NOT = qw(Devel::Optic::Lens::Perlish Devel::Optic);
13              
14 3     3   18 use Devel::Optic::Lens::Perlish::Constants qw(:all);
  3         12  
  3         325  
15              
16 3     3   19 use Scalar::Util qw(looks_like_number);
  3         4  
  3         135  
17 3     3   394 use Ref::Util qw(is_arrayref is_hashref is_refref is_scalarref is_ref);
  3         1319  
  3         3518  
18              
19             sub run {
20 55     55 0 85 my ($scope, $ast) = @_;
21 55         97 my ($type, $payload) = @$ast;
22 55 100       88 if ($type eq OP_ACCESS) {
23 41         74 return _access($scope, undef, $ast, $payload);
24             }
25              
26 14 50       21 if ($type eq SYMBOL) {
27 14         24 return _symbol($scope, $payload);
28             }
29              
30 0         0 croak sprintf("invalid query: %s does not start with access or symbol",
31             _ast_to_code($ast),
32             );
33             }
34              
35             sub _access {
36 80     80   120 my ($scope, $parent, $self, $children) = @_;
37              
38 80         103 my ($left, $right) = @$children;
39              
40 80         90 my ($l_arg, $r_arg);
41 80         122 my ($l_type, $l_val) = @$left;
42 80         106 my ($r_type, $r_val) = @$right;
43              
44 80 100       114 if ($l_type eq SYMBOL) {
45 50         78 $l_arg = _symbol($scope, $l_val);
46             }
47              
48 80 100       131 if ($l_type eq OP_ACCESS) {
49 30         45 $l_arg = _access($scope, $self, $left, $l_val);
50             }
51              
52 80 50       123 if ($r_type eq OP_ACCESS) {
53 0         0 die "an access can't be followed directly by another access. the parser admitted an invalid program. please report this!";
54             }
55              
56 80 100       120 if ($r_type eq OP_HASHKEY) {
57 38         86 return _hashkey($scope, $self, $left, $right, $l_arg, $r_val);
58             }
59              
60 42 50       85 if ($r_type eq OP_ARRAYINDEX) {
61 42         74 return _arrayindex($scope, $self, $left, $right, $l_arg, $r_val, $left);
62             }
63             }
64              
65             sub _arrayindex {
66 42     42   70 my ($scope, $access_node, $array_node, $index_node, $arrayref, $child) = @_;
67 42         58 my ($type, $value) = @$child;
68              
69 42 100       67 if (!is_arrayref($arrayref)) {
70 2         6 croak sprintf("invalid array access: '%s' is %s, not array",
71             _ast_to_code($array_node),
72             _sample_or_ref($arrayref),
73             );
74             }
75              
76 40 50       60 if ($type eq STRING) {
77 0         0 croak sprintf("invalid array access: can't index '%s' with string '%s'",
78             _ast_to_code($array_node),
79             $value,
80             );
81             }
82              
83 40         69 my $index;
84 40 100       55 if ($type eq NUMBER) {
85 27         31 $index = $value;
86             }
87              
88 40 100       66 if ($type eq SYMBOL) {
89 9         14 my $resolved = _symbol($scope, $value);
90 9 100       27 if (!looks_like_number($resolved)) {
91 2         5 croak sprintf("invalid array index in '%s': %s (not a number)",
92             _ast_to_code($access_node),
93             _resolved_node_or_literal($child, $resolved),
94             );
95             }
96              
97 7         10 $index = $resolved;
98             }
99              
100 38 100       56 if ($type eq OP_ACCESS) {
101 4         8 my $resolved = _access($scope, $index_node, $child, $value);
102 4 50       11 if (!looks_like_number($resolved)) {
103 0         0 croak sprintf("invalid array index in '%s': %s (not a number)",
104             _ast_to_code($access_node),
105             _resolved_node_or_literal($child, $resolved),
106             );
107             }
108 4         6 $index = $resolved;
109             }
110              
111 38 50       60 if (defined $index) {
112 38         43 my $len = scalar @$arrayref;
113             # negative indexes need checking too
114 38 100 100     110 if ($len <= $index || ($index < 0 && ((-1 * $index) > $len))) {
      100        
115 4         9 croak sprintf("out of bounds: index %s, but len(%s) == %s",
116             _resolved_node_or_literal($child, $index),
117             _ast_to_code($array_node),
118             $len,
119             );
120             }
121              
122 34         140 return $arrayref->[$index];
123             }
124              
125             # this should only happen when the parser admits an invalid program. which should never happen. in theory.
126 0         0 die "array index unexpected contents '$type'. please report this, it's a bug in the parser that this query was allowed in";
127             }
128              
129             sub _hashkey {
130 38     38   73 my ($scope, $access_node, $hash_node, $index_node, $hashref, $key_node) = @_;
131 38         53 my ($type, $value) = @$key_node;
132              
133 38 100       61 if (!is_hashref($hashref)) {
134 2 100       6 croak sprintf("invalid hash access: '%s' is %s, not hash",
135             _ast_to_code($hash_node),
136             defined $hashref ? _sample_or_ref($hashref) : "undef",
137             );
138             }
139              
140 36         43 my $key;
141 36 100 66     83 if ($type eq STRING || $type eq NUMBER) {
142 24         31 $key = $value;
143             }
144              
145 36 100       56 if ($type eq SYMBOL) {
146 7         13 my $resolved = _symbol($scope, $value);
147 7 100       13 if (is_ref($resolved)) {
148 1         3 croak sprintf("invalid hash key in '%s': %s",
149             _ast_to_code($access_node),
150             _resolved_node_or_literal($key_node, $resolved),
151             );
152             }
153              
154 6         9 $key = $resolved;
155             }
156              
157 35 100       57 if ($type eq OP_ACCESS) {
158 5         9 my $resolved = _access($scope, $index_node, $key_node, $value);
159 5 50       9 if (is_ref($resolved)) {
160 0         0 my $type = ref $resolved;
161 0         0 my $code = _ast_to_code($value);
162 0         0 croak sprintf("%s is a(n) %s ref. can't use this to index into hash %s",
163             _ast_to_code($key_node),
164             ref $resolved,
165             _ast_to_code($index_node)
166             );
167             }
168              
169 5         8 $key = $resolved;
170             }
171              
172 35 50       51 if (defined $key) {
173 35 100       59 if (!exists $hashref->{$key}) {
174 3         7 my $hash_source = _ast_to_code($hash_node);
175 3         5 my $key_source = _ast_to_code($key_node);
176 3         6 my $key_type = $key_node->[NODE_TYPE];
177 3   66     9 my $is_primitive = $key_type eq STRING || $key_type eq NUMBER;
178 3         13 croak sprintf("invalid hash key: %s is not in %s",
179             _resolved_node_or_literal($key_node, $key),
180             _ast_to_code($hash_node)
181             );
182             }
183              
184 32         125 return $hashref->{$key};
185             }
186              
187             # this should only happen when the parser admits an invalid program. which should never happen. in theory.
188 0         0 die "hash key unexpected contents '$type'. please report this, it's a bug in the parser that this query was allowed in";
189             }
190              
191             sub _symbol {
192 80     80   108 my ($scope, $name) = @_;
193              
194 80 100       393 croak "no symbol '$name' in scope" if !exists $scope->{$name};
195 77         104 my $val = $scope->{$name};
196 77 100 66     205 if (is_refref($val) || is_scalarref($val)) {
197 15         36 return $$val;
198             }
199              
200 62         126 return $val;
201             }
202              
203             sub _resolved_node_or_literal {
204 10     10   18 my ($node, $value) = @_;
205 10         20 my ($type, $payload) = @$node[NODE_TYPE,NODE_PAYLOAD];
206 10 100 100     54 if ($type eq STRING || $type eq NUMBER) {
207 3         14 return sprintf "'%s'", $payload;
208             }
209              
210 7         11 return sprintf("%s == '%s'",
211             _ast_to_code($node),
212             _sample_or_ref($value),
213             );
214             }
215              
216             sub _ast_to_code {
217 36     36   54 my ($op) = @_;
218 36         58 my ($type, $value) = @$op;
219 36 100 66     857 return $value if ($type eq SYMBOL || $type eq NUMBER);
220 7 100       12 return "'$value'" if $type eq STRING;
221              
222 6 100       14 return '{' . _ast_to_code($value) . '}' if $type eq OP_HASHKEY;
223 5 100       17 return '[' . _ast_to_code($value) . ']' if $type eq OP_ARRAYINDEX;
224              
225 3 50       9 if ($type eq OP_ACCESS) {
226 3         5 my ($left, $right) = @$value;
227 3         15 return sprintf("%s->%s", _ast_to_code($left), _ast_to_code($right));
228             }
229             }
230              
231             sub _sample_or_ref {
232 10     10   16 my $raw = shift;
233              
234 10 100       20 if (!defined $raw) {
235 1         90 return "undefined";
236             }
237              
238 9 100       14 if (is_ref($raw)) {
239 4         385 return ref($raw) . "REF";
240             }
241              
242 5 50       10 if ($raw eq "") {
243 0         0 return "(empty string)";
244             }
245              
246 5 50       9 if (length $raw > RAW_DATA_SAMPLE_SIZE) {
247 0         0 return sprintf("'%s'", substr($raw, 0, RAW_DATA_SAMPLE_SIZE) . "...");
248             }
249 5         110 return $raw;
250             }
251              
252             1;
253              
254             __END__