-
Notifications
You must be signed in to change notification settings - Fork 108
Gotchas
There are cases where our compiler will generate invalid bytecode or otherwise behaves in surprising ways. Though they are not all bugs, strictly speaking, we track them as notable bugs and document them here with known workarounds. The planned solution to most of these problems, particularly the ones without known workarounds, is the completion of the MAGIC compiler.
Tracked in #294
Mutating a closed-over field in a deftype
form is a little finicky. The following does not work in ClojureCLR or ClojureJVM.
(defprotocol P
(foo [this]))
(deftype B [^:unsynchronized-mutable ^int a]
P
(foo [this]
(set! a 20)))
(foo (->B 3))
The problem is the expression (set! a 20)
is trying to assign a value of type long
(the constant 20
) to a field of type int
. The Clojure compiler does not emit conversion bytecode in the case of a closed-over field, for some reason.
Using interop forces the compiler to emit conversion bytecode
(deftype B [^:unsynchronized-mutable ^int a]
P
(foo [this]
(set! (.a this) 20))) ;; <= (.a this) instead of a
Adjusting the type of the field of the type of the value allows the use of the closed-over field as a symbol. Both of these work
(deftype B [^:unsynchronized-mutable ^long a] ;; <= type hint changed from int to long
P
(foo [this]
(set! a 20)))
(deftype B [^:unsynchronized-mutable ^int a]
P
(foo [this]
(set! a (int 20)))) ;; <= casting to int
This throws IllegalArgumentException No matching field found: a for class user.B
on ClojureJVM but works on ClojureCLR, despite defying type safety.
(defprotocol P
(foo [this]))
(deftype B [^:unsynchronized-mutable ^String a]
P
(foo [this]
(set! (.a this) 20)))
(let [b (->B "albatross")]
(set! (.a b) :milkman)
(.a b))
Tracked in #264
(deftype Wazzoom [])
(type |System.Collections.Generic.Dictionary`2[System.Object,user.Wazzoom]|)
Throws
FileNotFoundException Could not load file or assembly 'deftype15229' or one of its dependencies. The system cannot find the file specified. System.AppDomain.Load (/Users/builduser/buildslave/mono/build/mcs/class/corlib/System/AppDomain.cs:692)
There is no known workaround.
Tracked in #114
by-ref
is used to pass arguments by reference in ClojureCLR, similar to the ref
keyword in C#
. It is buggy, however. The types must match exactly and the compiler will not always correctly flow type the information it has. In the following example ToAngleAxis
uses one normal parameter and two out
parameters which require by-ref
.
(ns by-ref.angle-axis
(:use arcadia.core)
(:import [UnityEngine Vector3 Quaternion]))
(defn v3 ^Vector3 [x y z]
(Vector3. x y z))
(defn angle-axis [^Quaternion q]
(let [ang (float 0) ;; ang is known to be a float
axis (v3 1 2 3)] ;; axis should be known to be a Vector3, but is not
(.ToAngleAxis q (by-ref ang) (by-ref axis))
[ang axis]))
(angle-axis (Quaternion/Euler (v3 10 10 10)))
;; Object reference not set to an instance of an object
Unfortunately even type hinting axis
as Vector3
will not help. Note that the following works
(defn angle-axis [^Quaternion q]
(let [ang (float 0)
axis Vector3/zero] ;; <= axis now know to be a Vector3
(.ToAngleAxis q (by-ref ang) (by-ref axis))
[ang axis]))
(angle-axis (Quaternion/Euler (v3 10 10 10)))
;; => [16.78646 #<UnityEngine.Vector3 (0.6, 0.5, 0.5)>]
There are no known workarounds, though this does work in MAGIC.
Tracked in #98
Unity's API makes extensive use of Coroutines. These are generated in C# using the yield
keyword. As this is a C# compiler feature and not a feature of the CLR itself, ClojureCLR cannot make use of it directly.
Unity Coroutines compile to IEnumerator
s, and we can use reify
to create them as needed.
(reify IEnumerator
(MoveNext [this]
;; this code will run once every "tick"
;; the value returned here will determine whether to keep running the coroutine or not
;; true -> schedule this coroutine again according to the value returned by get_Current
;; false -> do not schedule this coroutine again, it is finished
)
(get_Current [this]
;; the value returned here will determine when the next tick is
;; nil -> next frame
;; WaitForSeconds instance -> the number of seconds passed to the constructor
;; WWW instance -> until the resource has downloaded
;; IEnumerator instance -> will be run once that coroutine is finished
))
The final wrinkle is in how they are started. The StartCoroutine
method is a member of the MonoBehaviour
class, which means you need a reference to a Unity script component to run your Coroutines. This component will act as the "root" of the Coroutines, and if it is destroyed they will all stop.