Skip to content
Ramsey Nasser edited this page Dec 4, 2018 · 3 revisions

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.

Mutation in deftype

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.

Workaround: Use this

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

Workaround: Adjust the types

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

Inconsistency with ClojureJVM

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))

Types from deftype Cannot Be Used In Generic Types

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.

by-ref Types Must Match Exactly

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.

Coroutines

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.

Workaround: reify

Unity Coroutines compile to IEnumerators, 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.

Clone this wiki locally