Browse Source

Add haxe.runtime.Copy (#11863)

* 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
Simon Krajewski 8 months ago
parent
commit
c85b00d40f

+ 239 - 0
std/haxe/runtime/Copy.hx

@@ -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 - 0
tests/unit/src/unit/issues/Issue11863.hx

@@ -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 - 0
tests/unit/src/unitstd/haxe/runtime/Copy.unit.hx

@@ -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";