I have been wanting to write a raybench implementation in the Factor programming language for a long time, but only until now I got around to it, and well, the results are interesting.
Factor is a stack-based general purpose programming language, which is quite different to others that I am used to. Here is a simple example of a function that adds two numbers:
: sum ( x x -- x ) + ;
Which is deceptively simple, the
( x x -- x ) indicates the number of entries there should be in the stack before calling the function, and what the state of the stack is going to be after calling. And I say deceptively simple because with more involved functions, things start getting much more complicated, most probably becuase of my inexperience with this language.
The raybench implementation in Factor here is quite different to others built on top of more traditional languages but also quite interesting. Getting this to work took much more time than I was expecting, complications included debugging and keeping track of the stack at all times, still, it works as expected and it is faster than other implementations (like python, which is a cool turn of events), but still several times slower than the Rust implementation.
Factor has support for modules, here called vocabularies, consisting of words which are how functions are referred to.
All words have a shape that determines how many entries it expects to be in the stack prior to its execution, and how will the stack look afterwards. This is actually very useful when coding, as it will allow the compiler to detect unexpected stack changes.
The stack state is the most important thing to have in mind while working with the language, all operations are expected to interact with it by either taking values out of it or pushing values to it. I am mostly used to interacting with LIFO stacks, and that makes this language seem quite constrained but to make up for it, Factor has some special words for improved flexibility, like dip that accepts a
quotation (meaning one or more operations) that will be executed while ignoring the value at the top of the stack, think about it as poping the stack, saving that value somewhere in memory, execute the
quotation and when done, push the saved value back into the stack.
While developing the Factor implementation for raybench I found myself approaching the code very differently than other languages, for instance, I spend a lot of time in the Listener playing with the stack and seeing how different words modified it. There are also several functions that are oposite, for example there is
-rot that will negate each other, those kind of operations are great when testing code and you want to go back to a previous stack state and try different things.
I also liked how the language approaches errors, for instance, when you try to execute a lot of functions that are buggy, in the sense that the stack state is not as expected, the stack will not be modified and the Listener will let you know what the expectation is and how the current implementation diverges. This is very useful while working with quotations passed to words like
On the otherhand, you have to be very mindful about the order of the values passed in the stack when implementing a function, because if the first value you need is not the one at the top of the stack then a lot of shuffling around will happen, and if for some reason you need to change the order of the word parameters, a big refactor or a lot of shuffling might be required.
Also, I always forgot to add an empty space before the semicolon used to indicate the end of a word, i.e.
;, just to be greeted by a compile error.
Overall I think it was a fun experiment, learned some interesting things and forced me to think totally different compared with other implementations. Having said that I don't think I'll be coding any other project in Factor any time soon.