Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add haxe.runtime.Copy #11863

Merged
merged 11 commits into from
Dec 16, 2024
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";
Loading