Let it lie
I missed a week (SSSHHAAAME) because I didn’t get much of note done. I got mapping over tables working, along with destructuring of flat record data but the output -v- how much time I was sitting in front of the machine simply didnt add up.
I’ve decided to stop fighting my brain and just put the project down for a bit. This sucks as it means I fail my November goal but I just have to accept that I either need to force myself to do something my brain isn’t enjoying (which isn’t the best way to wind down after work) or do something else. At the very least I get to confirm things I have been learning about how I learn/work, so that is some kind of positive I can scrape from this.
With this accepted I started looking at first-class functions in my shader compiler (Varjo).
It’s been a month since I touched this, so I spent a little time re-familiarizing myself with the problem and then I got to work.
First order of business was to get funcall
working with variables of function type that are in scope. Something like this:
(let ((x #'foo))
(let ((y x))
(funcall y 10)))
I got the logic working for the above and then I spent a few hours making some parts of my compiler more strict about types. Some areas were just too forgiving about whether you had to provide a type object or let you pass a type signature instead. This made some other code more complicated that it needed to be. This was a relic from a much older version of the compiler.
I then spent some time thinking about how to handle passing functions to functions. I can use my flow analyzer and multiple passes but I don’t want to use that hammer if things can be easier.
For example let’s take this:
(labels ((foo ((f (function (:int) :int))) ;; take a func from :int -> :int and call it with 10
(funcall f 10))
(bar ((x :int)) ;; some func from :int -> :int
(* x 2)))
;; do it
(funcall #'foo #'bar))
I can replace this the (funcall #'foo #'bar)
with this:
(labels ((foo ()
(let ((f #'bar))
(funcall f 10))))
(funcall #'foo))
which will get turned into
(labels ((foo ()
(bar 10)))
(funcall #'foo))
This means I generate a new local function for each call-site of a function that takes functions. The compiler will any remove duplicate definitions.
At this point it’s worth pointing out one of the design goals of this feature. Predictability. This code is valid lisp:
(defun pick-func ()
(if (< some-var 10)
#'func-a
#'func-b))
(defun do-stuff ()
(funcall (pick-func) 10))
But at runtime we can’t pass functions around, so the best we could do for the above is to return an int
and switch
based on that.
int pick_func() {
return (some_var < 10) ? 0 : 1;
}
void do_stuff () {
switch (pick_func()) {
case 0:
func_a(10);
break;
case 1:
func_b(10);
break;
}
}
This would work but this pattern can be slow if used too much. For now Varjo instead chooses to disallow this and make you implement it yourself. This means there are less cases where you are guessing what the compiler is up to if your code is slow. The compiler will be able to generate very precise error messages to explain what is happening in those cases.
That’s all for now. I’ve also got a bunch of ideas for this that are still very nebulous, I’ll write more as they become concrete.
Ciao