From e575e64a8dc2b4437ea611a0ca320249985703a9 Mon Sep 17 00:00:00 2001 From: Simon Krajewski Date: Mon, 9 Dec 2024 07:32:01 +0100 Subject: [PATCH 1/9] start working on haxe.Copy --- std/haxe/Copy.hx | 163 +++++++++++++++++++++++ tests/unit/src/unitstd/haxe/Copy.unit.hx | 34 +++++ 2 files changed, 197 insertions(+) create mode 100644 std/haxe/Copy.hx create mode 100644 tests/unit/src/unitstd/haxe/Copy.unit.hx diff --git a/std/haxe/Copy.hx b/std/haxe/Copy.hx new file mode 100644 index 00000000000..131e6c2d5b7 --- /dev/null +++ b/std/haxe/Copy.hx @@ -0,0 +1,163 @@ +package haxe; + +class Copy { + var cacheMap:Array; + var cache:Array; + + public function new() { + cacheMap = []; + cache = []; + } + + public function copyValue(v:Dynamic):Dynamic { + switch (Type.typeof(v)) { + case TNull, TInt, TFloat, TBool: + return v; + case TClass(c): + if (#if neko untyped c.__is_String #else c == String #end) + return v; + var index = getRefIndex(v); + if (index >= 0) + return cacheMap[index]; + switch (#if (neko || python) Type.getClassName(c) #else c #end) { + case #if (neko || python) "Array" #else cast Array #end: + var nv:Array = []; + cacheMap.push(nv); + #if (flash || python || hl) + var v:Array = v; + #end + var l = v.length; + for (i in 0...l) { + var e:Dynamic = v[i]; + if (e == null) + nv.push(null); + else + nv.push(copyValue(e)); + } + return nv; + case #if (neko || python) "haxe.ds.List" #else cast List #end: + var nv = new List(); + cacheMap.push(nv); + var v:List = v; + for (e in v) + nv.add(copyValue(e)); + return nv; + case #if (neko || python) "Date" #else cast Date #end: + return v; + case #if (neko || python) "haxe.ds.StringMap" #else cast haxe.ds.StringMap #end: + var nv = new haxe.ds.StringMap(); + cacheMap.push(nv); + var v:haxe.ds.StringMap = v; + for (k => e in v) + nv.set(k, copyValue(e)); + return nv; + case #if (neko || python) "haxe.ds.IntMap" #else cast haxe.ds.IntMap #end: + var nv = new haxe.ds.IntMap(); + cacheMap.push(nv); + var v:haxe.ds.IntMap = v; + for (k => e in v) + nv.set(k, copyValue(e)); + return nv; + case #if (neko || python) "haxe.ds.ObjectMap" #else cast haxe.ds.ObjectMap #end: + var nv = new haxe.ds.ObjectMap(); + cacheMap.push(nv); + var v:haxe.ds.ObjectMap = v; + for (k => e in v) { + #if (js || neko) + var id = Reflect.field(k, "__id__"); + Reflect.deleteField(k, "__id__"); + nv.set(k, copyValue(e)); + Reflect.setField(k, "__id__", id); + #else + nv.set(k, copyValue(e)); + #end + } + return nv; + case #if (neko || python) "haxe.io.Bytes" #else cast haxe.io.Bytes #end: + var v:haxe.io.Bytes = v; + var nv = v.sub(0, v.length); + cacheMap.push(nv); + return nv; + default: + var nv = Type.createEmptyInstance(c); + cacheMap.push(nv); + copyFields(v, nv); + return nv; + } + case TObject: + if (v is Class || v is Enum) + return v; + var index = getRefIndex(v); + if (index >= 0) + return cacheMap[index]; + var nv = {}; + cacheMap.push(nv); + copyFields(v, nv); + return nv; + case TEnum(e): + var index = getRefIndex(v); + if (index >= 0) + return cacheMap[index]; + var v:EnumValue = v; + var args = v.getParameters(); + if (args.length == 0) { + cacheMap.push(v); + return v; + } + var nv:Dynamic = Type.createEnumIndex(e, v.getIndex(), args); + var needCopy = false; + cacheMap.push(nv); + for (i => e in args) { + var e = copyValue(e); + #if neko + nv.args[i] = e; + #elseif php + nv.params[i] = e; + #elseif (js && !js_enums_as_arrays) + nv.__params__[i] = e; + #elseif js + nv[i + 2] = e; + #else + needCopy = true; + args[i] = e; + #end + } + // only for platforms that don't know how to modify enum after create + // as a result, this might break some circular depencies + if (needCopy) + nv = Type.createEnumIndex(e, v.getIndex(), args); + return nv; + default: + return v; // assume not mutable + } + } + + function getRefIndex(v:Dynamic) { + #if js + var vt = js.Syntax.typeof(v); + #end + for (i in 0...cache.length) { + #if js + var ci = cache[i]; + if (js.Syntax.typeof(ci) == vt && ci == v) + #else + if (cache[i] == v) + #end + return i; + } + cache.push(v); + return -1; + } + + function copyFields(v:Dynamic, nv:Dynamic) { + for (f in Reflect.fields(v)) { + var e = copyValue(Reflect.field(v, f)); + Reflect.setField(nv, f, e); + } + } + + public static function copy(v:T):T { + var s = new Copy(); + return s.copyValue(v); + } +} diff --git a/tests/unit/src/unitstd/haxe/Copy.unit.hx b/tests/unit/src/unitstd/haxe/Copy.unit.hx new file mode 100644 index 00000000000..d81d00b58dc --- /dev/null +++ b/tests/unit/src/unitstd/haxe/Copy.unit.hx @@ -0,0 +1,34 @@ +// Array + +var a = [1, 2]; +var b = haxe.Copy.copy(a); +1 == b[0]; +2 == b[1]; +a != b; +var c = [a, a]; +var d = haxe.Copy.copy(c); +d[0] != a; +d[1] != a; +d[0] == d[1]; +// Anon + +var a = {f1: 1, f2: 2}; +var b = haxe.Copy.copy(a); +1 == b.f1; +2 == b.f2; +a != b; +var c = {f1: a, f2: a}; +var d = haxe.Copy.copy(c); +d.f1 != a; +d.f2 != a; +d.f1 == d.f2; +// Enum + +var a = (macro 1); +var b = haxe.Copy.copy(a); +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}'); +} From f2b4bd6803b822188448d7aeac8310d0bc8dc90b Mon Sep 17 00:00:00 2001 From: Simon Krajewski Date: Wed, 11 Dec 2024 09:52:30 +0100 Subject: [PATCH 2/9] rewrite --- std/haxe/Copy.hx | 228 ++++++++++------------- tests/unit/src/unitstd/haxe/Copy.unit.hx | 49 +++++ 2 files changed, 149 insertions(+), 128 deletions(-) diff --git a/std/haxe/Copy.hx b/std/haxe/Copy.hx index 131e6c2d5b7..d2bc3fc0e7b 100644 --- a/std/haxe/Copy.hx +++ b/std/haxe/Copy.hx @@ -1,152 +1,124 @@ package haxe; +import haxe.ds.StringMap; +import haxe.ds.IntMap; +import haxe.ds.ObjectMap; +import haxe.io.Bytes; + class Copy { - var cacheMap:Array; - var cache:Array; + // TODO: check __id__ stuff on JS/neko + var cacheMap:ObjectMap<{}, {}>; + var cacheMapLength:Int; - public function new() { - cacheMap = []; - cache = []; + function new() { + cacheMap = new ObjectMap(); + cacheMapLength = 0; } - public function copyValue(v:Dynamic):Dynamic { - switch (Type.typeof(v)) { - case TNull, TInt, TFloat, TBool: - return v; + function copyValue(v:T):T { + return switch (Type.typeof(v)) { + case TNull, TInt, TFloat, TBool, TClass(String | Date): + v; case TClass(c): - if (#if neko untyped c.__is_String #else c == String #end) - return v; - var index = getRefIndex(v); - if (index >= 0) - return cacheMap[index]; - switch (#if (neko || python) Type.getClassName(c) #else c #end) { - case #if (neko || python) "Array" #else cast Array #end: - var nv:Array = []; - cacheMap.push(nv); - #if (flash || python || hl) - var v:Array = v; - #end - var l = v.length; - for (i in 0...l) { - var e:Dynamic = v[i]; - if (e == null) - nv.push(null); - else - nv.push(copyValue(e)); + var v:O = cast v; + var vCopy = getRef(v); + if (vCopy != null) { + return vCopy; + } + switch (c) { + case Array: + var a = []; + cacheMap.set(v, a); + var v:Array = cast v; + for (x in v) { + if (x == null) { + a.push(null); + } else { + a.push(copyValue(x)); + } + } + cast a; + case haxe.ds.StringMap: + var map = new StringMap(); + cacheMap.set(v, map); + var v:StringMap = cast v; + for (k => v in v) { + map.set(k, copyValue(v)); } - return nv; - case #if (neko || python) "haxe.ds.List" #else cast List #end: - var nv = new List(); - cacheMap.push(nv); - var v:List = v; - for (e in v) - nv.add(copyValue(e)); - return nv; - case #if (neko || python) "Date" #else cast Date #end: - return v; - case #if (neko || python) "haxe.ds.StringMap" #else cast haxe.ds.StringMap #end: - var nv = new haxe.ds.StringMap(); - cacheMap.push(nv); - var v:haxe.ds.StringMap = v; - for (k => e in v) - nv.set(k, copyValue(e)); - return nv; - case #if (neko || python) "haxe.ds.IntMap" #else cast haxe.ds.IntMap #end: - var nv = new haxe.ds.IntMap(); - cacheMap.push(nv); - var v:haxe.ds.IntMap = v; - for (k => e in v) - nv.set(k, copyValue(e)); - return nv; - case #if (neko || python) "haxe.ds.ObjectMap" #else cast haxe.ds.ObjectMap #end: - var nv = new haxe.ds.ObjectMap(); - cacheMap.push(nv); - var v:haxe.ds.ObjectMap = v; - for (k => e in v) { - #if (js || neko) - var id = Reflect.field(k, "__id__"); - Reflect.deleteField(k, "__id__"); - nv.set(k, copyValue(e)); - Reflect.setField(k, "__id__", id); - #else - nv.set(k, copyValue(e)); - #end + cast map; + case haxe.ds.IntMap: + var map = new IntMap(); + cacheMap.set(v, map); + var v:IntMap = cast v; + for (k => v in v) { + map.set(k, copyValue(v)); } - return nv; - case #if (neko || python) "haxe.io.Bytes" #else cast haxe.io.Bytes #end: - var v:haxe.io.Bytes = v; + cast map; + case haxe.ds.ObjectMap: + var map = new ObjectMap(); + cacheMap.set(v, map); + var v:ObjectMap<{}, Dynamic> = cast v; + for (k => v in v) { + // TODO: check the __id__ situation + map.set(copyValue(k), copyValue(v)); + } + cast map; + case haxe.io.Bytes: + var v:Bytes = cast v; var nv = v.sub(0, v.length); - cacheMap.push(nv); - return nv; - default: - var nv = Type.createEmptyInstance(c); - cacheMap.push(nv); - copyFields(v, nv); - return nv; + cacheMap.set(v, nv); + cast nv; + case _: + vCopy = Type.createEmptyInstance(c); + cacheMap.set(v, vCopy); + copyFields(v, vCopy); + vCopy; } case TObject: - if (v is Class || v is Enum) + if (v is Class || v is Enum) { return v; - var index = getRefIndex(v); - if (index >= 0) - return cacheMap[index]; - var nv = {}; - cacheMap.push(nv); - copyFields(v, nv); - return nv; - case TEnum(e): - var index = getRefIndex(v); - if (index >= 0) - return cacheMap[index]; - var v:EnumValue = v; - var args = v.getParameters(); + } + var v:O = cast v; + var vCopy = getRef(v); + if (vCopy != null) { + return vCopy; + } + var o:O = cast {}; + cacheMap.set(v, o); + copyFields(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) { - cacheMap.push(v); + cacheMap.set(v, v); return v; } - var nv:Dynamic = Type.createEnumIndex(e, v.getIndex(), args); - var needCopy = false; - cacheMap.push(nv); - for (i => e in args) { - var e = copyValue(e); - #if neko - nv.args[i] = e; - #elseif php - nv.params[i] = e; - #elseif (js && !js_enums_as_arrays) - nv.__params__[i] = e; - #elseif js - nv[i + 2] = e; - #else - needCopy = true; - args[i] = e; - #end + var newArgs = []; + for (arg in args) { + // TODO: check wtf was happening here in the original implementation + newArgs.push(copyValue(arg)); } - // only for platforms that don't know how to modify enum after create - // as a result, this might break some circular depencies - if (needCopy) - nv = Type.createEnumIndex(e, v.getIndex(), args); - return nv; - default: - return v; // assume not mutable + var nv:O = cast Type.createEnumIndex(en, vEnumValue.getIndex(), newArgs); + cacheMap.set(v, nv); + nv; + case TUnknown | TFunction: + v; } } - function getRefIndex(v:Dynamic) { - #if js - var vt = js.Syntax.typeof(v); - #end - for (i in 0...cache.length) { - #if js - var ci = cache[i]; - if (js.Syntax.typeof(ci) == vt && ci == v) - #else - if (cache[i] == v) - #end - return i; + function getRef(v:T):T { + var vCopy = cacheMap.get(v); + if (vCopy != null) { + return cast vCopy; } - cache.push(v); - return -1; + return null; } function copyFields(v:Dynamic, nv:Dynamic) { diff --git a/tests/unit/src/unitstd/haxe/Copy.unit.hx b/tests/unit/src/unitstd/haxe/Copy.unit.hx index d81d00b58dc..3adc774b893 100644 --- a/tests/unit/src/unitstd/haxe/Copy.unit.hx +++ b/tests/unit/src/unitstd/haxe/Copy.unit.hx @@ -10,6 +10,19 @@ var d = haxe.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.Copy.copy(l); +1 == lCopy.pop(); +2 == lCopy.pop(); +l != lCopy; +var l = new haxe.ds.List(); +l.add(l); +var lCopy = haxe.Copy.copy(l); +l != lCopy; +lCopy == lCopy.pop(); // Anon var a = {f1: 1, f2: 2}; @@ -26,9 +39,45 @@ d.f1 == d.f2; var a = (macro 1); var b = haxe.Copy.copy(a); +a != b; +a.expr != b.expr; 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.Copy.copy(c); +c != d; +c.ref = c; +var d = haxe.Copy.copy(c); +c != d; +d == d.ref; +// StringMap +var map = new haxe.ds.StringMap(); +map.set("foo", map); +var mapCopy = haxe.Copy.copy(map); +map != mapCopy; +mapCopy == mapCopy.get("foo"); +// IntMap +var map = new haxe.ds.IntMap(); +map.set(0, map); +var mapCopy = haxe.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.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.Copy.copy(bytes); +bytes != bytesCopy; +bytesCopy.getString(0, 3) == "foo"; From a9e8488f65fedaa3036508a4320defef0a17c6f1 Mon Sep 17 00:00:00 2001 From: Simon Krajewski Date: Wed, 11 Dec 2024 11:33:59 +0100 Subject: [PATCH 3/9] bring back haxe.ds.List implementation --- std/haxe/Copy.hx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/std/haxe/Copy.hx b/std/haxe/Copy.hx index d2bc3fc0e7b..ad78c9a0e3c 100644 --- a/std/haxe/Copy.hx +++ b/std/haxe/Copy.hx @@ -3,6 +3,7 @@ package haxe; import haxe.ds.StringMap; import haxe.ds.IntMap; import haxe.ds.ObjectMap; +import haxe.ds.List; import haxe.io.Bytes; class Copy { @@ -39,6 +40,14 @@ class Copy { } } cast a; + case haxe.ds.List: + var l = new List(); + cacheMap.set(v, l); + var v:List = cast v; + for (x in v) { + l.add(copyValue(x)); + } + cast l; case haxe.ds.StringMap: var map = new StringMap(); cacheMap.set(v, map); From b35a94e22b5c9b139ec35448d55bb1618c6fd147 Mon Sep 17 00:00:00 2001 From: Simon Krajewski Date: Wed, 11 Dec 2024 11:56:10 +0100 Subject: [PATCH 4/9] make custom caching implementations for python and flash --- std/haxe/Copy.hx | 79 ++++++++++++++++++------ tests/unit/src/unitstd/haxe/Copy.unit.hx | 2 +- 2 files changed, 60 insertions(+), 21 deletions(-) diff --git a/std/haxe/Copy.hx b/std/haxe/Copy.hx index ad78c9a0e3c..6b1c0457249 100644 --- a/std/haxe/Copy.hx +++ b/std/haxe/Copy.hx @@ -6,14 +6,57 @@ import haxe.ds.ObjectMap; import haxe.ds.List; import haxe.io.Bytes; +// Python struggles with arrays as ObjectMap keys +// Flash struggles in general +#if (python || flash) +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 { // TODO: check __id__ stuff on JS/neko - var cacheMap:ObjectMap<{}, {}>; - var cacheMapLength:Int; + var cache:ObjectCache<{}>; function new() { - cacheMap = new ObjectMap(); - cacheMapLength = 0; + cache = new ObjectCache(); } function copyValue = cast v; for (x in v) { if (x == null) { @@ -42,7 +85,7 @@ class Copy { cast a; case haxe.ds.List: var l = new List(); - cacheMap.set(v, l); + cache.set(v, l); var v:List = cast v; for (x in v) { l.add(copyValue(x)); @@ -50,7 +93,7 @@ class Copy { cast l; case haxe.ds.StringMap: var map = new StringMap(); - cacheMap.set(v, map); + cache.set(v, map); var v:StringMap = cast v; for (k => v in v) { map.set(k, copyValue(v)); @@ -58,7 +101,7 @@ class Copy { cast map; case haxe.ds.IntMap: var map = new IntMap(); - cacheMap.set(v, map); + cache.set(v, map); var v:IntMap = cast v; for (k => v in v) { map.set(k, copyValue(v)); @@ -66,7 +109,7 @@ class Copy { cast map; case haxe.ds.ObjectMap: var map = new ObjectMap(); - cacheMap.set(v, map); + cache.set(v, map); var v:ObjectMap<{}, Dynamic> = cast v; for (k => v in v) { // TODO: check the __id__ situation @@ -76,11 +119,11 @@ class Copy { case haxe.io.Bytes: var v:Bytes = cast v; var nv = v.sub(0, v.length); - cacheMap.set(v, nv); + cache.set(v, nv); cast nv; case _: vCopy = Type.createEmptyInstance(c); - cacheMap.set(v, vCopy); + cache.set(v, vCopy); copyFields(v, vCopy); vCopy; } @@ -94,7 +137,7 @@ class Copy { return vCopy; } var o:O = cast {}; - cacheMap.set(v, o); + cache.set(v, o); copyFields(v, o); o; case TEnum(en): @@ -106,7 +149,7 @@ class Copy { } var args = vEnumValue.getParameters(); if (args.length == 0) { - cacheMap.set(v, v); + cache.set(v, v); return v; } var newArgs = []; @@ -115,19 +158,15 @@ class Copy { newArgs.push(copyValue(arg)); } var nv:O = cast Type.createEnumIndex(en, vEnumValue.getIndex(), newArgs); - cacheMap.set(v, nv); + cache.set(v, nv); nv; case TUnknown | TFunction: v; } } - function getRef(v:T):T { - var vCopy = cacheMap.get(v); - if (vCopy != null) { - return cast vCopy; - } - return null; + inline function getRef(v:T):T { + return cast cache.get(v); } function copyFields(v:Dynamic, nv:Dynamic) { diff --git a/tests/unit/src/unitstd/haxe/Copy.unit.hx b/tests/unit/src/unitstd/haxe/Copy.unit.hx index 3adc774b893..2d4f2b248ab 100644 --- a/tests/unit/src/unitstd/haxe/Copy.unit.hx +++ b/tests/unit/src/unitstd/haxe/Copy.unit.hx @@ -40,7 +40,7 @@ d.f1 == d.f2; var a = (macro 1); var b = haxe.Copy.copy(a); a != b; -a.expr != b.expr; +// 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); From 6297a9feff4de70d7660d340d02abebf3c5341d4 Mon Sep 17 00:00:00 2001 From: Simon Krajewski Date: Wed, 11 Dec 2024 12:08:16 +0100 Subject: [PATCH 5/9] copy wonky flash code from Serializer --- std/haxe/Copy.hx | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/std/haxe/Copy.hx b/std/haxe/Copy.hx index 6b1c0457249..219ec086e43 100644 --- a/std/haxe/Copy.hx +++ b/std/haxe/Copy.hx @@ -7,8 +7,7 @@ import haxe.ds.List; import haxe.io.Bytes; // Python struggles with arrays as ObjectMap keys -// Flash struggles in general -#if (python || flash) +#if python private class ObjectCache { var from:Array; var to:Array; @@ -124,7 +123,11 @@ class Copy { case _: vCopy = Type.createEmptyInstance(c); cache.set(v, vCopy); + #if flash + copyClassFields(v, vCopy, c); + #else copyFields(v, vCopy); + #end vCopy; } case TObject: @@ -176,6 +179,20 @@ class Copy { } } + #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 + public static function copy(v:T):T { var s = new Copy(); return s.copyValue(v); From ba14e5bef1df45396eb3f54bcf31a62c8ca36d57 Mon Sep 17 00:00:00 2001 From: Simon Krajewski Date: Wed, 11 Dec 2024 12:25:21 +0100 Subject: [PATCH 6/9] don't use ObjectMap on js and neko to avoid __id__ nonsense --- std/haxe/Copy.hx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/std/haxe/Copy.hx b/std/haxe/Copy.hx index 219ec086e43..72d1d06d350 100644 --- a/std/haxe/Copy.hx +++ b/std/haxe/Copy.hx @@ -7,7 +7,8 @@ import haxe.ds.List; import haxe.io.Bytes; // Python struggles with arrays as ObjectMap keys -#if python +// Neko and js add __id__ which isn't great +#if (python || js || neko) private class ObjectCache { var from:Array; var to:Array; @@ -51,7 +52,6 @@ private class ObjectCache { #end class Copy { - // TODO: check __id__ stuff on JS/neko var cache:ObjectCache<{}>; function new() { @@ -111,7 +111,6 @@ class Copy { cache.set(v, map); var v:ObjectMap<{}, Dynamic> = cast v; for (k => v in v) { - // TODO: check the __id__ situation map.set(copyValue(k), copyValue(v)); } cast map; From 4c822be98b4be6f00aa24b153a4b57d71aff0524 Mon Sep 17 00:00:00 2001 From: Simon Krajewski Date: Wed, 11 Dec 2024 13:01:40 +0100 Subject: [PATCH 7/9] defer inner recursion to preserve identity --- std/haxe/Copy.hx | 71 ++++++++++++++++-------- tests/unit/src/unit/issues/Issue11863.hx | 30 ++++++++++ 2 files changed, 77 insertions(+), 24 deletions(-) create mode 100644 tests/unit/src/unit/issues/Issue11863.hx diff --git a/std/haxe/Copy.hx b/std/haxe/Copy.hx index 72d1d06d350..412e671c0b4 100644 --- a/std/haxe/Copy.hx +++ b/std/haxe/Copy.hx @@ -53,9 +53,15 @@ private class ObjectCache { class Copy { var cache:ObjectCache<{}>; + var workList:Array<() -> Void>; function new() { cache = new ObjectCache(); + workList = []; + } + + function defer(f:() -> Void) { + workList.push(f); } function copyValue = cast v; - for (x in v) { - if (x == null) { - a.push(null); - } else { - a.push(copyValue(x)); + 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; - for (x in v) { - l.add(copyValue(x)); - } + 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; - for (k => v in v) { - map.set(k, copyValue(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; - for (k => v in v) { - map.set(k, copyValue(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; - for (k => v in v) { - map.set(copyValue(k), copyValue(v)); - } + defer(() -> { + for (k => v in v) { + map.set(copyValue(k), copyValue(v)); + } + }); cast map; case haxe.io.Bytes: var v:Bytes = cast v; @@ -123,9 +139,9 @@ class Copy { vCopy = Type.createEmptyInstance(c); cache.set(v, vCopy); #if flash - copyClassFields(v, vCopy, c); + defer(copyClassFields.bind(v, vCopy, c)); #else - copyFields(v, vCopy); + defer(copyFields.bind(v, vCopy)); #end vCopy; } @@ -140,7 +156,7 @@ class Copy { } var o:O = cast {}; cache.set(v, o); - copyFields(v, o); + defer(copyFields.bind(v, o)); o; case TEnum(en): var v:O = cast v; @@ -156,7 +172,6 @@ class Copy { } var newArgs = []; for (arg in args) { - // TODO: check wtf was happening here in the original implementation newArgs.push(copyValue(arg)); } var nv:O = cast Type.createEnumIndex(en, vEnumValue.getIndex(), newArgs); @@ -178,6 +193,12 @@ class Copy { } } + 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); @@ -193,7 +214,9 @@ class Copy { #end public static function copy(v:T):T { - var s = new Copy(); - return s.copyValue(v); + 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..99a49093032 --- /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.Copy.copy(e); + t(checkIdentity(e2)); + } +} From c0d212db4c8a79f20dc43bbd3ccb880ac074d8c5 Mon Sep 17 00:00:00 2001 From: Simon Krajewski Date: Wed, 11 Dec 2024 13:14:45 +0100 Subject: [PATCH 8/9] document --- std/haxe/Copy.hx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/std/haxe/Copy.hx b/std/haxe/Copy.hx index 412e671c0b4..f032252fe38 100644 --- a/std/haxe/Copy.hx +++ b/std/haxe/Copy.hx @@ -213,6 +213,23 @@ class Copy { } #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.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); From e21722293caefedc25349168ad06dc97d4016a57 Mon Sep 17 00:00:00 2001 From: Simon Krajewski Date: Mon, 16 Dec 2024 13:25:01 +0100 Subject: [PATCH 9/9] move to haxe.runtime.Copy --- std/haxe/{ => runtime}/Copy.hx | 4 +-- tests/unit/src/unit/issues/Issue11863.hx | 2 +- .../unitstd/haxe/{ => runtime}/Copy.unit.hx | 26 +++++++++---------- 3 files changed, 16 insertions(+), 16 deletions(-) rename std/haxe/{ => runtime}/Copy.hx (98%) rename tests/unit/src/unitstd/haxe/{ => runtime}/Copy.unit.hx (72%) diff --git a/std/haxe/Copy.hx b/std/haxe/runtime/Copy.hx similarity index 98% rename from std/haxe/Copy.hx rename to std/haxe/runtime/Copy.hx index f032252fe38..f22e13b70b6 100644 --- a/std/haxe/Copy.hx +++ b/std/haxe/runtime/Copy.hx @@ -1,4 +1,4 @@ -package haxe; +package haxe.runtime; import haxe.ds.StringMap; import haxe.ds.IntMap; @@ -223,7 +223,7 @@ class Copy { * boolean values * strings * functions - * type and enum references (e.g. `haxe.Copy`, `haxe.ds.Option`) + * type and enum references (e.g. `haxe.runtime.Copy`, `haxe.ds.Option`) * instances of Date * enum values without arguments diff --git a/tests/unit/src/unit/issues/Issue11863.hx b/tests/unit/src/unit/issues/Issue11863.hx index 99a49093032..ee32fdf7a31 100644 --- a/tests/unit/src/unit/issues/Issue11863.hx +++ b/tests/unit/src/unit/issues/Issue11863.hx @@ -24,7 +24,7 @@ class Issue11863 extends Test { var e = C(r); r.f = e; t(checkIdentity(e)); - var e2 = haxe.Copy.copy(e); + var e2 = haxe.runtime.Copy.copy(e); t(checkIdentity(e2)); } } diff --git a/tests/unit/src/unitstd/haxe/Copy.unit.hx b/tests/unit/src/unitstd/haxe/runtime/Copy.unit.hx similarity index 72% rename from tests/unit/src/unitstd/haxe/Copy.unit.hx rename to tests/unit/src/unitstd/haxe/runtime/Copy.unit.hx index 2d4f2b248ab..0606cb253f6 100644 --- a/tests/unit/src/unitstd/haxe/Copy.unit.hx +++ b/tests/unit/src/unitstd/haxe/runtime/Copy.unit.hx @@ -1,12 +1,12 @@ // Array var a = [1, 2]; -var b = haxe.Copy.copy(a); +var b = haxe.runtime.Copy.copy(a); 1 == b[0]; 2 == b[1]; a != b; var c = [a, a]; -var d = haxe.Copy.copy(c); +var d = haxe.runtime.Copy.copy(c); d[0] != a; d[1] != a; d[0] == d[1]; @@ -14,31 +14,31 @@ d[0] == d[1]; var l = new haxe.ds.List(); l.add(1); l.add(2); -var lCopy = haxe.Copy.copy(l); +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.Copy.copy(l); +var lCopy = haxe.runtime.Copy.copy(l); l != lCopy; lCopy == lCopy.pop(); // Anon var a = {f1: 1, f2: 2}; -var b = haxe.Copy.copy(a); +var b = haxe.runtime.Copy.copy(a); 1 == b.f1; 2 == b.f2; a != b; var c = {f1: a, f2: a}; -var d = haxe.Copy.copy(c); +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.Copy.copy(a); +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] { @@ -49,35 +49,35 @@ switch [a.expr, b.expr] { } // Class var c = new MyClass(0); -var d = haxe.Copy.copy(c); +var d = haxe.runtime.Copy.copy(c); c != d; c.ref = c; -var d = haxe.Copy.copy(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.Copy.copy(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.Copy.copy(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.Copy.copy(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.Copy.copy(bytes); +var bytesCopy = haxe.runtime.Copy.copy(bytes); bytes != bytesCopy; bytesCopy.getString(0, 3) == "foo";