Strongly typed (def)s in Xronos
Yesterday, I commited a changeset that greatly alters how (def)s and Vars are handled in Xronos. Previously, the Var class was modeled after the Clojure Var class. The Var class contained a root value of type object and for thread bindings it used a Box class (that basically wrapped a value of type object). The Var class provides the basic container for all global variables.
I was reading about Clojure's recent support for ahead of time compilation, and I noticed a few subtle differences between Var resolution in Clojure and in Xronos. Clojure resolves Vars at compile time and Xronos resolved them at runtime, so I decided to fix Xronos so it matched the behavior of Clojure. The change as simple enough and now the following code which would fail in Clojure also fails in Xronos:
(defmacro y [] x) (def x 5) (pr (+ 6 (y)))
This fails be cause y references the Var x before it is (def)ed, and in order to compile y we have to resolve the Var x.
As I was making this change, I realized that I now had access to the exact Var that was going to be used at runtime. This means that I have more options of what I can do with it as far as static compilation is concerned. So I broke Var into two classes VarBase and Var<T>. VarBase has the Name and Namespace properties and a few others, but Var<T> handles the value and a few other things. I also replaced Box with Box<T>. By default (def) will create a Var<object> which is functionally identical to the old Var class, but if you add metadata specifying the type then it will create a Var<T> of the specified type. This code will print 11 in both Xronos and Clojure:
(def #^{ :type System.Int32 } x 5)
(pr (+ 6 x))The difference is that in Xronos, the + is compiled to a integer add, while in Clojure, the + is forced to do dynamic addition. This example is not so great, since it tends to defeat the dynamic nature of the language, but in cases where the extra bit of performance is needed, then strongly typed Vars are great! There is also a very common case were it makes sense.
(defn add5 [x] (+ x 5)) (defmacro monkey [& expr] `(str "mon" ~@expr "key"))
Both of these result in a call to (def), and in Xronos it creates a Var<IFunction>. The advantage of this is that we now know at compile time that the Var<T> contains an IFunction. So (add5 6) now gets compiled to the eqivalent of:
varAdd5.get().Invoke((object)6);
instead of
((IFunction)varAdd5.get()).Invoke((object)6);
That simple change results in one less cast per form, and cuts startup time by about 30%. I thought about making def always infer and set the type of Vars, but the problem with that is that many types have compatible dynamic interfaces, and so no type makes sense but object.
