beneficent winds and enmarvelorated satiation, success is come
[gitmo/MooseX-Antlers.git] / tracer.pl
CommitLineData
e4d56f95 1
2BEGIN {
3
db1fc20b 4package Visitor::NameTracking;
e4d56f95 5
db1fc20b 6use Moose::Role;
e4d56f95 7use B qw(perlstring);
e4d56f95 8use namespace::clean -except => 'meta';
9
e4d56f95 10has '_current_trace_name' => (is => 'ro');
11has 'root_name' => (is => 'ro');
12
e4d56f95 13around visit => sub {
14 my ($orig, $self) = (shift, shift);
15 local $self->{_current_trace_name}
16 = ($self->{_current_trace_name}||$self->root_name);
17 return $self->$orig(@_);
18};
19
20around visit_hash_entry => sub {
21 my ($orig, $self) = (shift, shift);
22 my $key = $_[0]; # $key, $value
23 local $self->{_current_trace_name}
24 = $self->{_current_trace_name}.'->{'.(perlstring $key).'}';
25 return $self->$orig(@_);
26};
27
28around visit_array_entry => sub {
29 my ($orig, $self) = (shift, shift);
30 my $index = $_[1]; # $value, $index
31 local $self->{_current_trace_name}
32 = $self->{_current_trace_name}.'->['.$index.']';
33 return $self->$orig(@_);
34};
35
36around visit_scalar => sub {
37 my ($orig, $self) = (shift, shift);
38 local $self->{_current_trace_name} = '${'.$self->{_current_trace_name}.'}';
39 return $self->$orig(@_);
40};
41
db1fc20b 42package Ref::Tracer;
43
44use Moose;
45use Scalar::Util qw(weaken refaddr);
46use namespace::clean -except => 'meta';
47
48extends 'Data::Visitor';
49
50with 'Visitor::NameTracking';
51
3aa08904 52# dump the lazy when we get a sensible version of D::V on the dev system
53
db1fc20b 54has '_traced_refs' => (is => 'ro', lazy => 1, default => sub { {} });
55has '_traced_names' => (is => 'ro', lazy => 1, default => sub { {} });
56
57before visit_ref => sub {
58 my ($self, $data) = @_;
59
60 # can't just rely on refaddr because it may get re-used if the data goes
61 # out of scope (we could play clever games with free magic on the wizard
62 # or whatever but KISS) - but we -can- keep a weak reference which will
63 # turn to undef if the variable disappears
64
65 weaken($self->_traced_refs->{refaddr $data} = $data);
66
67 $self->_traced_names->{refaddr $data} = $self->_current_trace_name;
68};
69
e4d56f95 70sub traced_ref_map {
71 my $self = shift;
72 my $refs = $self->_traced_refs;
73 my $names = $self->_traced_names;
74
75 # nuke keys where the traced refs entry is undef since they indicate
76 # "went out of scope" so the name is no longer valid. however if we
77 # do still have a refs entry we know the name is valid because if it
78 # didn't go out of scope that refaddr can't have been re-used.
79 # (NB: I don't care if this works under ithreads)
80
81 delete @{$names}{grep !defined($refs->{$_}), keys %$names};
82 $names;
83}
84
85# force recursion into objects (Data::Visitor doesn't by default)
86
87sub visit_object { shift->visit_ref(@_) }
88
db1fc20b 89package Ref::Replacer;
90
91# note: we actually handle weaken as well as external refs because I intend
92# to use Data::Dumper as a first pass and YAML::XS as a second and neither
93# of them know how to deal with weak references
94#
9831d165 95# better still, neither actually does the bloody cross refs properly - Dumper
96# emits them but the nature of the beast is that they don't eval back in
97# right, YAML::XS seems to make two aliases to one ref (Data::Alias-ish)
98# since when I weaken one both copies disappear on me. sigh.
99#
100# on the upside, we can use a really dumb dumper for the rest - JSON::XS
101# strikes me as an interesting possibility for speed reasons
e4d56f95 102
3aa08904 103use Moose;
3aa08904 104use Scalar::Util qw(refaddr isweak);
105use namespace::clean -except => 'meta';
106
107extends 'Data::Visitor';
108
9b97c7a2 109# we need name tracking but have to apply the role at the end of the file
110# so that our around modifiers end up within the name tracking around
111# instead of outside - otherwise e.g. array value weakening goes wrong
3aa08904 112
113has 'external_mappings' => (is => 'ro', lazy => 1, default => sub { {} });
9831d165 114has '_internal_mappings' => (is => 'ro', lazy => 1, default => sub { {} });
3aa08904 115has 'weaken_these' => (is => 'ro', lazy => 1, default => sub { {} });
116has 'map_these' => (is => 'ro', lazy => 1, default => sub { {} });
117
9831d165 118around visit => sub {
119 my ($orig, $self) = (shift, shift);
120 my $value = $_[0];
121
122 # note that we can't localize this one since it needs to be global
123 # across the entire structure - we could consider a weakref based trick
124 # like we use in the recorder but I don't -think- there's any need
125
126 # if we've already seen this reference, register a mapping for this
127 # copy of it so we fix it up afterwards (see visit_ref for the same process
128 # being used for references to be supplied externally at deserialize time
129 # and the top of the class for notes on how much I love serializers)
130
131 if (ref($value) && (my $m = $self->_internal_mappings->{refaddr $value})) {
132 $self->map_these->{$self->_current_trace_name} = $m;
133 return undef;
134 }
135
136 return $self->$orig(@_);
137};
138
9b97c7a2 139around visit_ref => sub {
140 my ($orig, $self) = (shift, shift);
141 my $value = $_[0];
3aa08904 142
143 # if we've got a mapping for a reference (i.e. it's supplied from
144 # somewhere else) then we need to record where we are and then
145 # return undef for the fmap process so we serialize an undefined
146 # value and the fixup puts the external reference back in later
147
9b97c7a2 148 if (my $m = $self->external_mappings->{refaddr $value}) {
3aa08904 149 $self->map_these->{$self->_current_trace_name} = $m;
150 return undef;
151 }
152
9831d165 153 $self->_internal_mappings->{refaddr $value} = $self->_current_trace_name;
154
9b97c7a2 155 return $self->$orig(@_);
156};
157
158around visit_hash_value => sub {
159 my ($orig, $self) = (shift, shift);
160 my ($value, $key, $hash) = @_;
161 if (isweak $hash->{$key}) {
162 $self->weaken_these->{$self->_current_trace_name} = 1;
163 }
164 return $self->$orig(@_);
165};
3aa08904 166
9b97c7a2 167around visit_array_entry => sub {
168 my ($orig, $self) = (shift, shift);
169 my ($value, $index, $array) = @_;
170 if (isweak $array->[$index]) {
171 $self->weaken_these->{$self->_current_trace_name} = 1;
172 }
173 return $self->$orig(@_);
174};
175
176around visit_scalar => sub {
177 my ($orig, $self) = (shift, shift);
178 my $scalar = $_[0];
179 if (isweak $$scalar) {
180 $self->weaken_these->{$self->_current_trace_name} = 1;
181 }
182 return $self->$orig(@_);
183};
184
185# now it's safe to apply the role
186
187with 'Visitor::NameTracking';
d8576cdc 188
189sub fixup_code {
190 my $self = shift;
191 join("\n\n",
192 grep defined,
193 map $self->${\"_fixup_code_for_$_"},
194 qw(externals weakrefs)
195 );
196}
197
198sub _fixup_code_for_externals {
199 my $self = shift;
200 my $ext = $self->map_these;
201 return unless keys %$ext;
202 join("\n",
203 qq{# fixup code for external references},
204 map {
9831d165 205 my ($l, $r) = ($_, $ext->{$_});
206 # if the LHS is a scalarref deref then we actually
207 # need to strip that bit off and push the enref to the RHS since
208 # ${\undef} = "foo"
209 # is an attempt to modify a readonly value and perl will burst into tears
210 if ($l =~ m/^\${(.*)}$/) { $l = $1; $r = "\\".$r; }
211 $l.' = '.$r.';';
d8576cdc 212 }
213 sort keys %$ext
214 );
215}
216
217sub _fixup_code_for_weakrefs {
218 my $self = shift;
219 my $weaken = $self->weaken_these;
220 return unless keys %$weaken;
221 join("\n",
222 qq{# fixup code for weak references},
223 'use Scalar::Util ();',
224 map {
225 'Scalar::Util::weaken('.$_.');';
226 }
227 sort keys %$weaken
228 );
229}
230
3aa08904 231# force recursion into objects (Data::Visitor doesn't by default)
232
233sub visit_object { shift->visit_ref(@_) }
e4d56f95 234
235}
236
237use Test::More qw(no_plan);
9831d165 238use Scalar::Util qw(refaddr weaken isweak);
239use YAML::XS;
e4d56f95 240
241my $foo = {
242 bar => { baz => [ 'quux', { fleem => 1 } ] },
243 skald => \[ { hot => 'story' } ],
244};
245
246my @expect = split "\n", <<'EOEXPECT';
247$foo
248$foo->{"bar"}
249$foo->{"skald"}
250${$foo->{"skald"}}
251${$foo->{"skald"}}->[0]
252EOEXPECT
253
254my $tracer = Ref::Tracer->new({ root_name => '$foo' });
255
256$tracer->visit($foo);
257
258delete $foo->{bar}{baz};
259
260my $result = $tracer->traced_ref_map;
261
262is_deeply(
263 \@expect,
264 [ sort { length($a) <=> length($b) } values %$result ],
265 "Expected results present"
266);
267
268my %map = reverse %$result;
269
270foreach my $e (@expect) {
271 my $value = do {
272 local $@;
273 my $r = eval $e;
274 die "Error $@ evaluating $e" if $@;
275 $r;
276 };
277 is($map{$e},refaddr($value), "Result for ${e} ok");
278}
279
d8576cdc 280my $flimflam = {
281 one => { two => three },
282 bard => $foo->{skald},
283 bard_guts => ${$foo->{skald}},
284 empty_now => $foo->{bar},
285};
286
287weaken($flimflam->{weak_one} = $flimflam->{one});
9b97c7a2 288weaken($flimflam->{weak_member}[0] = $flimflam->{bard});
289weaken(${$flimflam->{weak_scalar}} = $flimflam->{bard_guts});
d8576cdc 290
9831d165 291#use Data::Dumper; $Data::Dumper::Indent = 1;
292
293#warn "Flimflam:\n".Dumper($flimflam);
294
d8576cdc 295my $replacer = Ref::Replacer->new({
296 external_mappings => $result,
297 root_name => '$final',
298});
299
300my $copyflam = $replacer->visit($flimflam);
301
9831d165 302my $dump = Dump($copyflam);
303my $fixup = $replacer->fixup_code;
304
305#warn "Dump:\n".$dump;
306#warn "Fixup:\n".$fixup;
307
308my $final = Load($dump);
309
310#warn "Unfixed final:\n".Dumper($final);
311
312{
313 local $@;
314 eval $fixup;
315 die "fixup code died: $@" if $@;
316}
317
318#warn "Fixed final:\n".Dumper($final);
d8576cdc 319
9831d165 320is_deeply($flimflam, $final, 'Structures deeply the same after fixup');
d8576cdc 321
9831d165 322ok(isweak($final->{weak_one}), '$final->{weak_one} is a weak ref');
323ok(isweak($final->{weak_member}[0]), '$final->{weak_member}[0] is a weak ref');
324ok(isweak(${$final->{weak_scalar}}), '${$final->{weak_scalar}} is a weak ref');