Skip to content

Commit

Permalink
Add haxe.runtime.Copy (#11863)
Browse files Browse the repository at this point in the history
* start working on haxe.Copy

* rewrite

* bring back haxe.ds.List implementation

* make custom caching implementations for python and flash

* copy wonky flash code from Serializer

* don't use ObjectMap on js and neko to avoid __id__ nonsense

* defer inner recursion to preserve identity

* document

* move to haxe.runtime.Copy
  • Loading branch information
Simn authored Dec 16, 2024
1 parent 52d1bdd commit c85b00d
Show file tree
Hide file tree
Showing 3 changed files with 352 additions and 0 deletions.
239 changes: 239 additions & 0 deletions std/haxe/runtime/Copy.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
package haxe.runtime;

import haxe.ds.StringMap;
import haxe.ds.IntMap;
import haxe.ds.ObjectMap;
import haxe.ds.List;
import haxe.io.Bytes;

// Python struggles with arrays as ObjectMap keys
// Neko and js add __id__ which isn't great
#if (python || js || neko)
private class ObjectCache<K:{}> {
var from:Array<K>;
var to:Array<K>;

public function new() {
from = [];
to = [];
}

public function get(k:K) {
for (i => v in from) {
if (v == k) {
return to[i];
}
}
return null;
}

public function set(k:K, v:K) {
var index = from.length;
from[index] = k;
to[index] = v;
}
}
#else
private class ObjectCache<K:{}> {
var cache:ObjectMap<K, K>;

public function new() {
cache = new ObjectMap();
}

public inline function get(k:K) {
return cache.get(k);
}

public inline function set(k:K, v:K) {
cache.set(k, v);
}
}
#end

class Copy {
var cache:ObjectCache<{}>;
var workList:Array<() -> Void>;

function new() {
cache = new ObjectCache();
workList = [];
}

function defer(f:() -> Void) {
workList.push(f);
}

function copyValue<T, O:{}
& T>(v:T):T {
return switch (Type.typeof(v)) {
case TNull, TInt, TFloat, TBool, TClass(String | Date):
v;
case TClass(c):
var v:O = cast v;
var vCopy = getRef(v);
if (vCopy != null) {
return vCopy;
}
switch (c) {
case Array:
var a = [];
cache.set(v, a);
var v:Array<Dynamic> = cast v;
defer(() -> {
for (x in v) {
if (x == null) {
a.push(null);
} else {
a.push(copyValue(x));
}
}
});
cast a;
case haxe.ds.List:
var l = new List();
cache.set(v, l);
var v:List<Dynamic> = cast v;
defer(() -> {
for (x in v) {
l.add(copyValue(x));
}
});
cast l;
case haxe.ds.StringMap:
var map = new StringMap();
cache.set(v, map);
var v:StringMap<Dynamic> = cast v;
defer(() -> {
for (k => v in v) {
map.set(k, copyValue(v));
}
});
cast map;
case haxe.ds.IntMap:
var map = new IntMap();
cache.set(v, map);
var v:IntMap<Dynamic> = cast v;
defer(() -> {
for (k => v in v) {
map.set(k, copyValue(v));
}
});
cast map;
case haxe.ds.ObjectMap:
var map = new ObjectMap();
cache.set(v, map);
var v:ObjectMap<{}, Dynamic> = cast v;
defer(() -> {
for (k => v in v) {
map.set(copyValue(k), copyValue(v));
}
});
cast map;
case haxe.io.Bytes:
var v:Bytes = cast v;
var nv = v.sub(0, v.length);
cache.set(v, nv);
cast nv;
case _:
vCopy = Type.createEmptyInstance(c);
cache.set(v, vCopy);
#if flash
defer(copyClassFields.bind(v, vCopy, c));
#else
defer(copyFields.bind(v, vCopy));
#end
vCopy;
}
case TObject:
if (v is Class || v is Enum) {
return v;
}
var v:O = cast v;
var vCopy = getRef(v);
if (vCopy != null) {
return vCopy;
}
var o:O = cast {};
cache.set(v, o);
defer(copyFields.bind(v, o));
o;
case TEnum(en):
var v:O = cast v;
var vEnumValue:EnumValue = cast v;
var vCopy = getRef(v);
if (vCopy != null) {
return vCopy;
}
var args = vEnumValue.getParameters();
if (args.length == 0) {
cache.set(v, v);
return v;
}
var newArgs = [];
for (arg in args) {
newArgs.push(copyValue(arg));
}
var nv:O = cast Type.createEnumIndex(en, vEnumValue.getIndex(), newArgs);
cache.set(v, nv);
nv;
case TUnknown | TFunction:
v;
}
}

inline function getRef<T:{}>(v:T):T {
return cast cache.get(v);
}

function copyFields(v:Dynamic, nv:Dynamic) {
for (f in Reflect.fields(v)) {
var e = copyValue(Reflect.field(v, f));
Reflect.setField(nv, f, e);
}
}

function finalize() {
while (workList.length > 0) {
workList.pop()();
}
}

#if flash
function copyClassFields(v:Dynamic, nv:Dynamic, c:Dynamic) {
var xml:flash.xml.XML = untyped __global__["flash.utils.describeType"](c);
var vars = xml.factory[0].child("variable");
for (i in 0...vars.length()) {
var f = vars[i].attribute("name").toString();
if (!v.hasOwnProperty(f))
continue;
var e = copyValue(Reflect.field(v, f));
Reflect.setField(nv, f, e);
}
}
#end

/**
Creates a deep copy of `v`.
The following values remain unchanged:
* null
* numeric values
* boolean values
* strings
* functions
* type and enum references (e.g. `haxe.runtime.Copy`, `haxe.ds.Option`)
* instances of Date
* enum values without arguments
Any other value `v` is recursively copied, ensuring
that `v != copy(v)` holds.
**/
public static function copy<T>(v:T):T {
var copy = new Copy();
var v = copy.copyValue(v);
copy.finalize();
return v;
}
}
30 changes: 30 additions & 0 deletions tests/unit/src/unit/issues/Issue11863.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package unit.issues;

private enum E {
C(r:R);
}

private typedef R = {
f:Null<E>
}

class Issue11863 extends Test {
function checkIdentity(e:E) {
switch (e) {
case C(r1):
return (e == r1.f);
}
return false;
}

function test() {
var r = {
f: null
};
var e = C(r);
r.f = e;
t(checkIdentity(e));
var e2 = haxe.runtime.Copy.copy(e);
t(checkIdentity(e2));
}
}
83 changes: 83 additions & 0 deletions tests/unit/src/unitstd/haxe/runtime/Copy.unit.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Array

var a = [1, 2];
var b = haxe.runtime.Copy.copy(a);
1 == b[0];
2 == b[1];
a != b;
var c = [a, a];
var d = haxe.runtime.Copy.copy(c);
d[0] != a;
d[1] != a;
d[0] == d[1];
// List
var l = new haxe.ds.List();
l.add(1);
l.add(2);
var lCopy = haxe.runtime.Copy.copy(l);
1 == lCopy.pop();
2 == lCopy.pop();
l != lCopy;
var l = new haxe.ds.List<Dynamic>();
l.add(l);
var lCopy = haxe.runtime.Copy.copy(l);
l != lCopy;
lCopy == lCopy.pop();
// Anon

var a = {f1: 1, f2: 2};
var b = haxe.runtime.Copy.copy(a);
1 == b.f1;
2 == b.f2;
a != b;
var c = {f1: a, f2: a};
var d = haxe.runtime.Copy.copy(c);
d.f1 != a;
d.f2 != a;
d.f1 == d.f2;
// Enum

var a = (macro 1);
var b = haxe.runtime.Copy.copy(a);
a != b;
// a.expr != b.expr; // this fails on cpp, but enum instance equality isn't very specified anyway
switch [a.expr, b.expr] {
case [EConst(CInt(a)), EConst(CInt(b))]:
eq(a, b);
case _:
utest.Assert.fail('match failure: ${a.expr} ${b.expr}');
}
// Class
var c = new MyClass(0);
var d = haxe.runtime.Copy.copy(c);
c != d;
c.ref = c;
var d = haxe.runtime.Copy.copy(c);
c != d;
d == d.ref;
// StringMap
var map = new haxe.ds.StringMap<Dynamic>();
map.set("foo", map);
var mapCopy = haxe.runtime.Copy.copy(map);
map != mapCopy;
mapCopy == mapCopy.get("foo");
// IntMap
var map = new haxe.ds.IntMap<Dynamic>();
map.set(0, map);
var mapCopy = haxe.runtime.Copy.copy(map);
map != mapCopy;
mapCopy == mapCopy.get(0);
// ObjectMap
var map = new haxe.ds.ObjectMap<{}, Dynamic>();
var key = {};
map.set(key, map);
var mapCopy = haxe.runtime.Copy.copy(map);
map != mapCopy;
var keyCopy = [for (key in mapCopy.keys()) key][0];
t(mapCopy == mapCopy.get(keyCopy));
key != keyCopy;
// Bytes
var bytes = haxe.io.Bytes.ofString("foo");
var bytesCopy = haxe.runtime.Copy.copy(bytes);
bytes != bytesCopy;
bytesCopy.getString(0, 3) == "foo";

0 comments on commit c85b00d

Please sign in to comment.