From 07755515de8566a61eeff4e8b0183d5ae0468fb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20N=C3=BCcke?= Date: Mon, 2 Jun 2014 00:24:48 +0200 Subject: [PATCH] Added support for recursive conversion (i.e. converters can push values that need to be converted again, such as item stacks) and support for pushing cyclic maps / lists. Closes #278. --- .../java/li/cil/oc/api/driver/Converter.java | 9 +- .../component/machine/luac/UserdataAPI.scala | 2 +- .../li/cil/oc/server/driver/Registry.scala | 134 ++++++++++++------ .../li/cil/oc/util/ExtendedLuaState.scala | 74 +++++----- 4 files changed, 137 insertions(+), 82 deletions(-) diff --git a/src/main/java/li/cil/oc/api/driver/Converter.java b/src/main/java/li/cil/oc/api/driver/Converter.java index c10cfa5d6..8eccc321c 100644 --- a/src/main/java/li/cil/oc/api/driver/Converter.java +++ b/src/main/java/li/cil/oc/api/driver/Converter.java @@ -12,12 +12,11 @@ import java.util.Map; */ public interface Converter { /** - * Converts a type to a Map that only contains valid values, i.e. values - * that can be directly pushed to an architecture, without further - * conversion steps. + * Converts a type to a Map. *

- * This is primarily enforced to avoid cycles in conversion steps. If the - * returned map contains any unsupported values, they will not be retained. + * The keys and values in the resulting map will be converted in turn. + * If after those conversions the map still contains unsupported values, + * they will not be retained. *

* The conversion result should be placed into the the passed map, i.e. the * map will represent the original object. For example, if the value had a diff --git a/src/main/scala/li/cil/oc/server/component/machine/luac/UserdataAPI.scala b/src/main/scala/li/cil/oc/server/component/machine/luac/UserdataAPI.scala index a09947c50..2867dc88f 100644 --- a/src/main/scala/li/cil/oc/server/component/machine/luac/UserdataAPI.scala +++ b/src/main/scala/li/cil/oc/server/component/machine/luac/UserdataAPI.scala @@ -83,7 +83,7 @@ class UserdataAPI(owner: NativeLuaArchitecture) extends NativeLuaAPI(owner) { lua.pushScalaFunction(lua => { val value = lua.toJavaObjectRaw(1).asInstanceOf[Value] - lua.pushTable(Callbacks(value).map(entry => entry._1 -> entry._2.direct)) + lua.pushValue(Callbacks(value).map(entry => entry._1 -> entry._2.direct)) 1 }) lua.setField(-2, "methods") diff --git a/src/main/scala/li/cil/oc/server/driver/Registry.scala b/src/main/scala/li/cil/oc/server/driver/Registry.scala index bddc0c140..ee708e78b 100644 --- a/src/main/scala/li/cil/oc/server/driver/Registry.scala +++ b/src/main/scala/li/cil/oc/server/driver/Registry.scala @@ -8,7 +8,8 @@ import li.cil.oc.{OpenComputers, api} import net.minecraft.item.ItemStack import net.minecraft.world.World import scala.collection.convert.WrapAsScala._ -import scala.collection.mutable.ArrayBuffer +import scala.collection.mutable +import scala.math.ScalaNumber /** * This class keeps track of registered drivers and provides installation logic @@ -25,11 +26,11 @@ import scala.collection.mutable.ArrayBuffer * the computer, but may also provide context-free functions. */ private[oc] object Registry extends api.detail.DriverAPI { - val blocks = ArrayBuffer.empty[api.driver.Block] + val blocks = mutable.ArrayBuffer.empty[api.driver.Block] - val items = ArrayBuffer.empty[api.driver.Item] + val items = mutable.ArrayBuffer.empty[api.driver.Item] - val converters = ArrayBuffer.empty[api.driver.Converter] + val converters = mutable.ArrayBuffer.empty[api.driver.Converter] /** Used to keep track of whether we're past the init phase. */ var locked = false @@ -59,48 +60,95 @@ private[oc] object Registry extends api.detail.DriverAPI { if (stack != null) items.find(_.worksWith(stack)).orNull else null - def convert(value: Array[AnyRef]) = if (value != null) value.map(convertRecursively) else null + def convert(value: Array[AnyRef]) = if (value != null) value.map(arg => convertRecursively(arg, new util.IdentityHashMap())) else null - def convertRecursively(value: AnyRef): AnyRef = value match { - case null | Unit | None => null - case arg: java.lang.Boolean => arg - case arg: java.lang.Byte => arg - case arg: java.lang.Character => arg - case arg: java.lang.Short => arg - case arg: java.lang.Integer => arg - case arg: java.lang.Long => arg - case arg: java.lang.Float => arg - case arg: java.lang.Double => arg - case arg: java.lang.String => arg - - case arg: Array[Boolean] => arg - case arg: Array[Byte] => arg - case arg: Array[Character] => arg - case arg: Array[Short] => arg - case arg: Array[Integer] => arg - case arg: Array[Long] => arg - case arg: Array[Float] => arg - case arg: Array[Double] => arg - case arg: Array[String] => arg - - case arg: Value => arg - - case arg: Array[_] => arg.map { - case (value: AnyRef) => convertRecursively(value) + def convertRecursively(value: Any, memo: util.IdentityHashMap[AnyRef, AnyRef], force: Boolean = false): AnyRef = { + val valueRef = value match { + case number: ScalaNumber => number.underlying + case reference: AnyRef => reference + case null => null + case primitive => primitive.asInstanceOf[AnyRef] } - case arg: Map[_, _] => arg.collect { - case (key: AnyRef, value: AnyRef) => convertRecursively(key) -> convertRecursively(value) - } - case arg: java.util.Map[_, _] => arg.collect { - case (key: AnyRef, value: AnyRef) => convertRecursively(key) -> convertRecursively(value) + if (!force && memo.containsKey(valueRef)) { + memo.get(valueRef) } + else valueRef match { + case null | Unit | None => null - case arg => - val result = new util.HashMap[AnyRef, AnyRef]() - converters.foreach(converter => try converter.convert(arg, result) catch { - case t: Throwable => OpenComputers.log.log(Level.WARNING, "Type converter threw an exception.", t) - }) - if (result.isEmpty) null - else result + case arg: java.lang.Boolean => arg + case arg: java.lang.Byte => arg + case arg: java.lang.Character => arg + case arg: java.lang.Short => arg + case arg: java.lang.Integer => arg + case arg: java.lang.Long => arg + case arg: java.lang.Float => arg + case arg: java.lang.Double => arg + case arg: java.lang.String => arg + + case arg: Array[Boolean] => arg + case arg: Array[Byte] => arg + case arg: Array[Character] => arg + case arg: Array[Short] => arg + case arg: Array[Integer] => arg + case arg: Array[Long] => arg + case arg: Array[Float] => arg + case arg: Array[Double] => arg + case arg: Array[String] => arg + + case arg: Value => arg + + case arg: Array[_] => convertList(arg, arg.zipWithIndex.iterator, memo) + case arg: Product => convertList(arg, arg.productIterator.zipWithIndex, memo) + case arg: Seq[_] => convertList(arg, arg.zipWithIndex.iterator, memo) + + case arg: Map[_, _] => convertMap(arg, arg, memo) + case arg: mutable.Map[_, _] => convertMap(arg, arg.toMap, memo) + case arg: java.util.Map[_, _] => convertMap(arg, arg.toMap, memo) + + case arg => + val converted = new util.HashMap[AnyRef, AnyRef]() + memo += arg -> converted + converters.foreach(converter => try converter.convert(arg, converted) catch { + case t: Throwable => OpenComputers.log.log(Level.WARNING, "Type converter threw an exception.", t) + }) + if (converted.isEmpty) { + memo += arg -> null + null + } + else { + // This is a little nasty but necessary because we need to keep the + // 'converted' value up-to-date for any reference created to it in + // the following convertRecursively call. For example: + // - Converter C is called for A with map M. + // - C puts A into M again. + // - convertRecursively(M) encounters A in the memoization map, uses M. + // That M is then 'wrong', as in not fully converted. Hence the clear + // plus copy action afterwards. + memo += converted -> converted // Makes convertMap re-use the map. + convertRecursively(converted, memo, force = true) + memo -= converted + converted + } + } + } + + def convertList(obj: AnyRef, list: Iterator[(Any, Int)], memo: util.IdentityHashMap[AnyRef, AnyRef]) = { + val converted = new Array[AnyRef](list.size) + memo += obj -> converted + for ((value, index) <- list) { + converted(index) = convertRecursively(value, memo) + } + converted + } + + def convertMap(obj: AnyRef, map: Map[_, _], memo: util.IdentityHashMap[AnyRef, AnyRef]) = { + val converted = memo.getOrElseUpdate(obj, mutable.Map.empty[AnyRef, AnyRef]) match { + case map: mutable.Map[AnyRef, AnyRef]@unchecked => map + case map: java.util.Map[AnyRef, AnyRef]@unchecked => mapAsScalaMap(map) + } + map.collect { + case (key: AnyRef, value: AnyRef) => converted += convertRecursively(key, memo) -> convertRecursively(value, memo) + } + memo.get(obj) } } diff --git a/src/main/scala/li/cil/oc/util/ExtendedLuaState.scala b/src/main/scala/li/cil/oc/util/ExtendedLuaState.scala index 806f58aea..de9274a7b 100644 --- a/src/main/scala/li/cil/oc/util/ExtendedLuaState.scala +++ b/src/main/scala/li/cil/oc/util/ExtendedLuaState.scala @@ -1,6 +1,7 @@ package li.cil.oc.util import com.naef.jnlua.{LuaType, JavaFunction, LuaState} +import java.util import li.cil.oc.api.machine.Value import li.cil.oc.OpenComputers import scala.collection.convert.WrapAsScala._ @@ -18,43 +19,49 @@ object ExtendedLuaState { override def invoke(state: LuaState) = f(state) }) - def pushValue(value: Any) { - (value match { - case number: ScalaNumber => number.underlying - case reference: AnyRef => reference - case null => null - case primitive => primitive.asInstanceOf[AnyRef] - }) match { - case null | Unit | _: BoxedUnit => lua.pushNil() - case value: java.lang.Boolean => lua.pushBoolean(value.booleanValue) - case value: java.lang.Byte => lua.pushNumber(value.byteValue) - case value: java.lang.Character => lua.pushString(String.valueOf(value)) - case value: java.lang.Short => lua.pushNumber(value.shortValue) - case value: java.lang.Integer => lua.pushNumber(value.intValue) - case value: java.lang.Long => lua.pushNumber(value.longValue) - case value: java.lang.Float => lua.pushNumber(value.floatValue) - case value: java.lang.Double => lua.pushNumber(value.doubleValue) - case value: java.lang.String => lua.pushString(value) - case value: Array[Byte] => lua.pushByteArray(value) - case value: Array[_] => pushList(value.zipWithIndex.iterator) - case value: Value => lua.pushJavaObjectRaw(value) - case value: Product => pushList(value.productIterator.zipWithIndex) - case value: Seq[_] => pushList(value.zipWithIndex.iterator) - case value: java.util.Map[_, _] => pushTable(value.toMap) - case value: Map[_, _] => pushTable(value) - case value: mutable.Map[_, _] => pushTable(value.toMap) - case _ => - OpenComputers.log.warning("Tried to push an unsupported value of type to Lua: " + value.getClass.getName + ".") - lua.pushNil() + def pushValue(value: Any, memo: util.IdentityHashMap[Any, Int] = new util.IdentityHashMap()) { + if (memo.containsKey(value)) { + lua.pushValue(memo.get(value)) + } + else { + (value match { + case number: ScalaNumber => number.underlying + case reference: AnyRef => reference + case null => null + case primitive => primitive.asInstanceOf[AnyRef] + }) match { + case null | Unit | _: BoxedUnit => lua.pushNil() + case value: java.lang.Boolean => lua.pushBoolean(value.booleanValue) + case value: java.lang.Byte => lua.pushNumber(value.byteValue) + case value: java.lang.Character => lua.pushString(String.valueOf(value)) + case value: java.lang.Short => lua.pushNumber(value.shortValue) + case value: java.lang.Integer => lua.pushNumber(value.intValue) + case value: java.lang.Long => lua.pushNumber(value.longValue) + case value: java.lang.Float => lua.pushNumber(value.floatValue) + case value: java.lang.Double => lua.pushNumber(value.doubleValue) + case value: java.lang.String => lua.pushString(value) + case value: Array[Byte] => lua.pushByteArray(value) + case value: Array[_] => pushList(value, value.zipWithIndex.iterator, memo) + case value: Value => lua.pushJavaObjectRaw(value) + case value: Product => pushList(value, value.productIterator.zipWithIndex, memo) + case value: Seq[_] => pushList(value, value.zipWithIndex.iterator, memo) + case value: java.util.Map[_, _] => pushTable(value, value.toMap, memo) + case value: Map[_, _] => pushTable(value, value, memo) + case value: mutable.Map[_, _] => pushTable(value, value.toMap, memo) + case _ => + OpenComputers.log.warning("Tried to push an unsupported value of type to Lua: " + value.getClass.getName + ".") + lua.pushNil() + } } } - def pushList(list: Iterator[(Any, Int)]) { + def pushList(obj: AnyRef, list: Iterator[(Any, Int)], memo: util.IdentityHashMap[Any, Int]) { lua.newTable() + memo += obj -> lua.getTop var count = 0 list.foreach { case (value, index) => - pushValue(value) + pushValue(value, memo) lua.rawSet(-2, index + 1) count = count + 1 } @@ -63,12 +70,13 @@ object ExtendedLuaState { lua.rawSet(-3) } - def pushTable(map: Map[_, _]) { + def pushTable(obj: AnyRef, map: Map[_, _], memo: util.IdentityHashMap[Any, Int]) { lua.newTable(0, map.size) + memo += obj -> lua.getTop for ((key: AnyRef, value: AnyRef) <- map) { if (key != null && key != Unit && !key.isInstanceOf[BoxedUnit]) { - pushValue(key) - pushValue(value) + pushValue(key, memo) + pushValue(value, memo) lua.setTable(-3) } }