Handle circular refs when converting from Perl

First half of CLOWNFISH-36.


Project: http://git-wip-us.apache.org/repos/asf/lucy-clownfish/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucy-clownfish/commit/3e0546fa
Tree: http://git-wip-us.apache.org/repos/asf/lucy-clownfish/tree/3e0546fa
Diff: http://git-wip-us.apache.org/repos/asf/lucy-clownfish/diff/3e0546fa

Branch: refs/heads/master
Commit: 3e0546fabc8a2c3953561e184fc3bc5d2a10a8f1
Parents: 6c8dbb3
Author: Nick Wellnhofer <wellnho...@aevum.de>
Authored: Tue Mar 8 11:54:42 2016 +0100
Committer: Nick Wellnhofer <wellnho...@aevum.de>
Committed: Thu Mar 10 14:29:29 2016 +0100

----------------------------------------------------------------------
 runtime/perl/t/binding/016-vector.t |  29 ++++++-
 runtime/perl/t/binding/017-hash.t   |  14 +++-
 runtime/perl/xs/XSBind.c            | 128 +++++++++++++++++++++++++++----
 3 files changed, 151 insertions(+), 20 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/lucy-clownfish/blob/3e0546fa/runtime/perl/t/binding/016-vector.t
----------------------------------------------------------------------
diff --git a/runtime/perl/t/binding/016-vector.t 
b/runtime/perl/t/binding/016-vector.t
index ff2e547..15c3942 100644
--- a/runtime/perl/t/binding/016-vector.t
+++ b/runtime/perl/t/binding/016-vector.t
@@ -16,8 +16,8 @@
 use strict;
 use warnings;
 
-use Test::More tests => 1;
-use Clownfish;
+use Test::More tests => 3;
+use Clownfish qw( to_clownfish );
 
 my ( $vector, $twin );
 
@@ -32,3 +32,28 @@ $vector->insert(
 $twin = $vector->clone_raw;
 is_deeply( $twin->to_perl, $vector->to_perl, "clone" );
 
+use Data::Dumper;
+
+my $hashref  = { foo => 'Foo', bar => 'Bar' };
+$hashref->{baz} = [ { circular => [ undef, $hashref ], one => 'One' } ];
+
+my $arrayref = [];
+push( @$arrayref, [] ) for 1..5000;
+push( @$arrayref, $arrayref, { key => $arrayref }, 42, $hashref, 'string' );
+
+$vector = to_clownfish($arrayref);
+is( $$vector, ${ $vector->fetch_raw(5000) },
+    'to_clownfish($arrayref) handles circular references' );
+
+my $hash = $vector->fetch_raw(5003);
+is(
+    $$hash,
+    ${
+        $hash->fetch_raw('baz')
+             ->fetch_raw(0)
+             ->fetch_raw('circular')
+             ->fetch_raw(1)
+    },
+    'to_clownfish($arrayref) handles deep circular references'
+);
+

http://git-wip-us.apache.org/repos/asf/lucy-clownfish/blob/3e0546fa/runtime/perl/t/binding/017-hash.t
----------------------------------------------------------------------
diff --git a/runtime/perl/t/binding/017-hash.t 
b/runtime/perl/t/binding/017-hash.t
index 9b26829..b69d8e7 100644
--- a/runtime/perl/t/binding/017-hash.t
+++ b/runtime/perl/t/binding/017-hash.t
@@ -16,7 +16,7 @@
 use strict;
 use warnings;
 
-use Test::More tests => 10;
+use Test::More tests => 12;
 use Clownfish qw( to_clownfish );
 
 my $hash = Clownfish::Hash->new( capacity => 10 );
@@ -45,3 +45,15 @@ my $round_tripped = to_clownfish( \%hash_with_utf8_keys 
)->to_perl;
 is_deeply( $round_tripped, \%hash_with_utf8_keys,
     "Round trip conversion of hash with UTF-8 keys" );
 
+my $hashref = {};
+$hashref->{foo} = $hashref;
+$hashref->{bar} = [ $hashref ];
+
+$hash = to_clownfish($hashref);
+is( $$hash, ${ $hash->fetch_raw('foo') },
+    'to_clownfish($hashref) handles circular references' );
+
+$hash = to_clownfish({ key => $hashref })->fetch_raw('key');
+is( $$hash, ${ $hash->fetch_raw('bar')->fetch_raw(0) },
+    'to_clownfish($hashref) handles deep circular references' );
+

http://git-wip-us.apache.org/repos/asf/lucy-clownfish/blob/3e0546fa/runtime/perl/xs/XSBind.c
----------------------------------------------------------------------
diff --git a/runtime/perl/xs/XSBind.c b/runtime/perl/xs/XSBind.c
index 85bd85b..a4f9046 100644
--- a/runtime/perl/xs/XSBind.c
+++ b/runtime/perl/xs/XSBind.c
@@ -30,6 +30,7 @@
 #include "Clownfish/HashIterator.h"
 #include "Clownfish/Method.h"
 #include "Clownfish/Num.h"
+#include "Clownfish/PtrHash.h"
 #include "Clownfish/TestHarness/TestUtils.h"
 #include "Clownfish/Util/Atomic.h"
 #include "Clownfish/Util/StringHelper.h"
@@ -38,19 +39,29 @@
 #define XSBIND_REFCOUNT_FLAG   1
 #define XSBIND_REFCOUNT_SHIFT  1
 
+// Used to remember converted objects in array and hash conversion to
+// handle circular references. The root object and SV are stored separately
+// to allow lazy creation of the seen PtrHash.
+typedef struct {
+    cfish_Obj     *root_obj;
+    SV            *root_sv;
+    cfish_PtrHash *seen;
+} cfish_ConversionCache;
+
 static bool
 S_maybe_perl_to_cfish(pTHX_ SV *sv, cfish_Class *klass, bool increment,
-                      void *allocation, cfish_Obj **obj_ptr);
+                      void *allocation, cfish_ConversionCache *cache,
+                      cfish_Obj **obj_ptr);
 
 // Convert a Perl hash into a Clownfish Hash.  Caller takes responsibility for
 // a refcount.
 static cfish_Hash*
-S_perl_hash_to_cfish_hash(pTHX_ HV *phash);
+S_perl_hash_to_cfish_hash(pTHX_ HV *phash, cfish_ConversionCache *cache);
 
 // Convert a Perl array into a Clownfish Vector.  Caller takes responsibility
 // for a refcount.
 static cfish_Vector*
-S_perl_array_to_cfish_array(pTHX_ AV *parray);
+S_perl_array_to_cfish_array(pTHX_ AV *parray, cfish_ConversionCache *cache);
 
 cfish_Obj*
 XSBind_new_blank_obj(pTHX_ SV *either_sv) {
@@ -103,7 +114,7 @@ XSBind_sv_true(pTHX_ SV *sv) {
 cfish_Obj*
 XSBind_perl_to_cfish(pTHX_ SV *sv, cfish_Class *klass) {
     cfish_Obj *retval = NULL;
-    if (!S_maybe_perl_to_cfish(aTHX_ sv, klass, true, NULL, &retval)) {
+    if (!S_maybe_perl_to_cfish(aTHX_ sv, klass, true, NULL, NULL, &retval)) {
         THROW(CFISH_ERR, "Can't convert to %o", CFISH_Class_Get_Name(klass));
     }
     else if (!retval) {
@@ -115,7 +126,7 @@ XSBind_perl_to_cfish(pTHX_ SV *sv, cfish_Class *klass) {
 cfish_Obj*
 XSBind_perl_to_cfish_nullable(pTHX_ SV *sv, cfish_Class *klass) {
     cfish_Obj *retval = NULL;
-    if (!S_maybe_perl_to_cfish(aTHX_ sv, klass, true, NULL, &retval)) {
+    if (!S_maybe_perl_to_cfish(aTHX_ sv, klass, true, NULL, NULL, &retval)) {
         THROW(CFISH_ERR, "Can't convert to %o", CFISH_Class_Get_Name(klass));
     }
     return retval;
@@ -124,7 +135,9 @@ XSBind_perl_to_cfish_nullable(pTHX_ SV *sv, cfish_Class 
*klass) {
 cfish_Obj*
 XSBind_perl_to_cfish_noinc(pTHX_ SV *sv, cfish_Class *klass, void *allocation) 
{
     cfish_Obj *retval = NULL;
-    if (!S_maybe_perl_to_cfish(aTHX_ sv, klass, false, allocation, &retval)) {
+    if (!S_maybe_perl_to_cfish(aTHX_ sv, klass, false, allocation, NULL,
+                               &retval)
+       ) {
         THROW(CFISH_ERR, "Can't convert to %o", CFISH_Class_Get_Name(klass));
     }
     else if (!retval) {
@@ -135,7 +148,8 @@ XSBind_perl_to_cfish_noinc(pTHX_ SV *sv, cfish_Class 
*klass, void *allocation) {
 
 static bool
 S_maybe_perl_to_cfish(pTHX_ SV *sv, cfish_Class *klass, bool increment,
-                      void *allocation, cfish_Obj **obj_ptr) {
+                      void *allocation, cfish_ConversionCache *cache,
+                      cfish_Obj **obj_ptr) {
     if (sv_isobject(sv)) {
         cfish_String *class_name = CFISH_Class_Get_Name(klass);
         // Assume that the class name is always NULL-terminated. Somewhat
@@ -161,13 +175,13 @@ S_maybe_perl_to_cfish(pTHX_ SV *sv, cfish_Class *klass, 
bool increment,
         if (inner_type == SVt_PVAV) {
             if (klass == CFISH_VECTOR || klass == CFISH_OBJ) {
                 obj = (cfish_Obj*)
-                         S_perl_array_to_cfish_array(aTHX_ (AV*)inner);
+                         S_perl_array_to_cfish_array(aTHX_ (AV*)inner, cache);
             }
         }
         else if (inner_type == SVt_PVHV) {
             if (klass == CFISH_HASH || klass == CFISH_OBJ) {
                 obj = (cfish_Obj*)
-                         S_perl_hash_to_cfish_hash(aTHX_ (HV*)inner);
+                         S_perl_hash_to_cfish_hash(aTHX_ (HV*)inner, cache);
             }
         }
         else if (inner_type < SVt_PVAV && !SvOK(inner)) {
@@ -250,10 +264,40 @@ XSBind_hash_key_to_utf8(pTHX_ HE *entry, STRLEN 
*size_ptr) {
 }
 
 static cfish_Hash*
-S_perl_hash_to_cfish_hash(pTHX_ HV *phash) {
+S_perl_hash_to_cfish_hash(pTHX_ HV *phash, cfish_ConversionCache *cache) {
+    cfish_ConversionCache new_cache;
+
+    if (cache) {
+        // Lookup perl hash in conversion cache.
+        if ((SV*)phash == cache->root_sv) {
+            return (cfish_Hash*)CFISH_INCREF(cache->root_obj);
+        }
+        if (cache->seen) {
+            void *cached_hash = CFISH_PtrHash_Fetch(cache->seen, phash);
+            if (cached_hash) {
+                return (cfish_Hash*)CFISH_INCREF(cached_hash);
+            }
+        }
+    }
+
     uint32_t    num_keys = hv_iterinit(phash);
     cfish_Hash *retval   = cfish_Hash_new(num_keys);
 
+    if (!cache) {
+        // Set up conversion cache.
+        cache = &new_cache;
+        cache->root_obj = (cfish_Obj*)retval;
+        cache->root_sv  = (SV*)phash;
+        cache->seen     = NULL;
+    }
+    else {
+        if (!cache->seen) {
+            // Create PtrHash lazily.
+            cache->seen = cfish_PtrHash_new(0);
+        }
+        CFISH_PtrHash_Store(cache->seen, phash, retval);
+    }
+
     while (num_keys--) {
         HE         *entry    = hv_iternext(phash);
         STRLEN      key_len  = 0;
@@ -261,31 +305,77 @@ S_perl_hash_to_cfish_hash(pTHX_ HV *phash) {
         SV         *value_sv = HeVAL(entry);
 
         // Recurse.
-        cfish_Obj *value
-            = XSBind_perl_to_cfish_nullable(aTHX_ value_sv, CFISH_OBJ);
+        cfish_Obj *value;
+        bool success = S_maybe_perl_to_cfish(aTHX_ value_sv, CFISH_OBJ,
+                                             true, NULL, cache, &value);
+        if (!success) {
+            THROW(CFISH_ERR, "Can't convert to Clownfish::Obj");
+        }
 
         CFISH_Hash_Store_Utf8(retval, key_str, key_len, value);
     }
 
+    if (cache == &new_cache && cache->seen) {
+        CFISH_PtrHash_Destroy(cache->seen);
+    }
+
     return retval;
 }
 
 static cfish_Vector*
-S_perl_array_to_cfish_array(pTHX_ AV *parray) {
+S_perl_array_to_cfish_array(pTHX_ AV *parray, cfish_ConversionCache *cache) {
+    cfish_ConversionCache new_cache;
+
+    if (cache) {
+        // Lookup perl array in conversion cache.
+        if ((SV*)parray == cache->root_sv) {
+            return (cfish_Vector*)CFISH_INCREF(cache->root_obj);
+        }
+        if (cache->seen) {
+            void *cached_vector = CFISH_PtrHash_Fetch(cache->seen, parray);
+            if (cached_vector) {
+                return (cfish_Vector*)CFISH_INCREF(cached_vector);
+            }
+        }
+    }
+
     const uint32_t  size   = av_len(parray) + 1;
     cfish_Vector   *retval = cfish_Vec_new(size);
 
+    if (!cache) {
+        // Set up conversion cache.
+        cache = &new_cache;
+        cache->root_obj = (cfish_Obj*)retval;
+        cache->root_sv  = (SV*)parray;
+        cache->seen     = NULL;
+    }
+    else {
+        if (!cache->seen) {
+            // Create PtrHash lazily.
+            cache->seen = cfish_PtrHash_new(0);
+        }
+        CFISH_PtrHash_Store(cache->seen, parray, retval);
+    }
+
     // Iterate over array elems.
     for (uint32_t i = 0; i < size; i++) {
         SV **elem_sv = av_fetch(parray, i, false);
         if (elem_sv) {
-            cfish_Obj *elem
-                = XSBind_perl_to_cfish_nullable(aTHX_ *elem_sv, CFISH_OBJ);
+            cfish_Obj *elem;
+            bool success = S_maybe_perl_to_cfish(aTHX_ *elem_sv, CFISH_OBJ,
+                                                 true, NULL, cache, &elem);
+            if (!success) {
+                THROW(CFISH_ERR, "Can't convert to Clownfish::Obj");
+            }
             if (elem) { CFISH_Vec_Store(retval, i, elem); }
         }
     }
     CFISH_Vec_Resize(retval, size); // needed if last elem is NULL
 
+    if (cache == &new_cache && cache->seen) {
+        CFISH_PtrHash_Destroy(cache->seen);
+    }
+
     return retval;
 }
 
@@ -391,7 +481,9 @@ XSBind_arg_to_cfish(pTHX_ SV *value, const char *label, 
cfish_Class *klass,
                     void *allocation) {
     cfish_Obj *obj = NULL;
 
-    if (!S_maybe_perl_to_cfish(aTHX_ value, klass, false, allocation, &obj)) {
+    if (!S_maybe_perl_to_cfish(aTHX_ value, klass, false, allocation, NULL,
+                               &obj)
+       ) {
         THROW(CFISH_ERR, "Invalid value for '%s' - not a %o", label,
               CFISH_Class_Get_Name(klass));
         CFISH_UNREACHABLE_RETURN(cfish_Obj*);
@@ -409,7 +501,9 @@ XSBind_arg_to_cfish_nullable(pTHX_ SV *value, const char 
*label,
                              cfish_Class *klass, void *allocation) {
     cfish_Obj *obj = NULL;
 
-    if (!S_maybe_perl_to_cfish(aTHX_ value, klass, false, allocation, &obj)) {
+    if (!S_maybe_perl_to_cfish(aTHX_ value, klass, false, allocation, NULL,
+                               &obj)
+       ) {
         THROW(CFISH_ERR, "Invalid value for '%s' - not a %o", label,
               CFISH_Class_Get_Name(klass));
         CFISH_UNREACHABLE_RETURN(cfish_Obj*);

Reply via email to