diff --git a/projects/core-api/src/main/java/dan200/computercraft/api/lua/MethodResult.java b/projects/core-api/src/main/java/dan200/computercraft/api/lua/MethodResult.java
index aec0f8615..8b834f54c 100644
--- a/projects/core-api/src/main/java/dan200/computercraft/api/lua/MethodResult.java
+++ b/projects/core-api/src/main/java/dan200/computercraft/api/lua/MethodResult.java
@@ -8,9 +8,7 @@ import dan200.computercraft.api.peripheral.IComputerAccess;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
-import java.util.Collection;
-import java.util.Map;
-import java.util.Objects;
+import java.util.*;
/**
* The result of invoking a Lua method.
@@ -55,6 +53,12 @@ public final class MethodResult {
*
* In order to provide a custom object with methods, one may return a {@link IDynamicLuaObject}, or an arbitrary
* class with {@link LuaFunction} annotations. Anything else will be converted to {@code nil}.
+ *
+ * Shared objects in a {@link MethodResult} will preserve their sharing when converted to Lua values. For instance,
+ * {@code Map, ?> m = new HashMap(); return MethodResult.of(m, m); } will return two values {@code a}, {@code b}
+ * where {@code a == b}. The one exception to this is Java's singleton collections ({@link List#of()},
+ * {@link Set#of()} and {@link Map#of()}), which are always converted to new table. This is not true for other
+ * singleton collections, such as those provided by {@link Collections} or Guava.
*
* @param value The value to return to the calling Lua function.
* @return A method result which returns immediately with the given value.
diff --git a/projects/core/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java b/projects/core/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java
index 2b97f00fb..5d51e55e0 100644
--- a/projects/core/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java
+++ b/projects/core/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java
@@ -13,6 +13,7 @@ import dan200.computercraft.core.Logging;
import dan200.computercraft.core.computer.TimeoutState;
import dan200.computercraft.core.methods.LuaMethod;
import dan200.computercraft.core.methods.MethodSupplier;
+import dan200.computercraft.core.util.LuaUtil;
import dan200.computercraft.core.util.Nullability;
import dan200.computercraft.core.util.SanitisedError;
import org.slf4j.Logger;
@@ -183,10 +184,35 @@ public class CobaltLuaMachine implements ILuaMachine {
return ValueFactory.valueOf(bytes);
}
+ // Don't share singleton values, and instead convert them to a new table.
+ if (LuaUtil.isSingletonCollection(object)) return new LuaTable();
+
if (values == null) values = new IdentityHashMap<>(1);
var result = values.get(object);
if (result != null) return result;
+ var wrapped = toValueWorker(object, values);
+ if (wrapped == null) {
+ LOG.warn(Logging.JAVA_ERROR, "Received unknown type '{}', returning nil.", object.getClass().getName());
+ return Constants.NIL;
+ }
+
+ values.put(object, wrapped);
+ return wrapped;
+ }
+
+ /**
+ * Convert a complex Java object (such as a collection or Lua object) to a Lua value.
+ *
+ * This is a worker function for {@link #toValue(Object, IdentityHashMap)}, which handles the actual construction
+ * of values, without reading/writing from the value map.
+ *
+ * @param object The object to convert.
+ * @param values The map of Java to Lua values.
+ * @return The converted value, or {@code null} if it could not be converted.
+ * @throws LuaError If the value could not be converted.
+ */
+ private @Nullable LuaValue toValueWorker(Object object, IdentityHashMap