diff --git a/std/haxe/runtime/Copy.hx b/std/haxe/runtime/Copy.hx new file mode 100644 index 00000000000..f22e13b70b6 --- /dev/null +++ b/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 { + var from:Array; + var to:Array; + + 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 { + var cache:ObjectMap; + + 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(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 = 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 = 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 = 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 = 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(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(v:T):T { + var copy = new Copy(); + var v = copy.copyValue(v); + copy.finalize(); + return v; + } +} diff --git a/tests/unit/src/unit/issues/Issue11863.hx b/tests/unit/src/unit/issues/Issue11863.hx new file mode 100644 index 00000000000..ee32fdf7a31 --- /dev/null +++ b/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 +} + +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)); + } +} diff --git a/tests/unit/src/unitstd/haxe/runtime/Copy.unit.hx b/tests/unit/src/unitstd/haxe/runtime/Copy.unit.hx new file mode 100644 index 00000000000..0606cb253f6 --- /dev/null +++ b/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(); +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(); +map.set("foo", map); +var mapCopy = haxe.runtime.Copy.copy(map); +map != mapCopy; +mapCopy == mapCopy.get("foo"); +// IntMap +var map = new haxe.ds.IntMap(); +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";