Port to inheritance-based object model #483
Open
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Overview
Around two years ago, I did some benchmarking of jank's previous object model versus the model we have today. Ultimately, the cost of vtable pointers was very high, when it came to allocating objects. On top of that, the number of interfaces which Clojure JVM objects implement is quite impractical in C++, and in some cases actually impossible.
The outcome was the model we have today, which is closed. Each object is a self-contained type and behaviors are determined via concepts. Type erasure is done by point to a member in each type object and that member holds an enum value of the type of that object. Given the enum, and a large
switch
, we can get back to the original type.The details are here: https://jank-lang.org/blog/2023-07-08-object-model/
Challenges
Our closed model is faster than the open model, hands down. But it sacrifices a lot.
Openness
Implementing Clojure protocols, records, and interfaces is going to be quite difficult, since the current model is closed.
Usability from C++
The visitor pattern usage we have, combined with concepts, is quite advanced and relies on C++20 knowledge. It also significantly impacts compile times, since each visitor function needs to be instantiated 50+ times (once for each object type).
Goals for this PR
I would like to add the open object model back in, on this branch, in order to benchmark how it looks in jank today. It will be slower, but how much slower? Slow enough to justify losing all of the openness and functionality and ease of use from C++? It might be. We may never merge this PR, since we'll decide that it's overall still worthwhile.
jank has changed significantly in the last two years, though. I think it's worth checking again.
Process
Here's a rough outline of what needs to be done in order for this branch to be successfully ported.
object
object
member from each typed objectdynamic_cast
associatively_readable
+associatively_writable
=>map_like
stackable
=>stack_like
comparable
collection_like
countable
indexable
nameable
=>named
chunk_like
chunkable
seqable
sequence_like
sequenceable_in_place
=>in_place_sequence_like
sequential
number_like
(leave this for me)persistentable
=>transient_like
transientable
=>editable
derefable
set_like
metadatable
callable
tofunction_like
type
fromobject
once we no longer have anyvisit_object
callsobj_type
from every typed objectruntime/visit.hpp
altogetherI suspect the behaviors above can be combined even further, but we can always combine more once we get things ported.
Porting a behavior
Firstly, ignore
math.cpp
. I will take care of it, since it's quite complex.In order to port a particular behavior, we need to do the following:
a. This involves adding the behavior as a base type and then updating the existing functions
i. Make sure the functions are marked
override
ii. Make sure the functions match the exact type from the interface; concepts are more lenient about this and we took advantage of it in some cases
visit_object
calls and start replacing them with RTTI checks insteadExample visit replacement
Before
After
Keep things working
Our goal here is to keep jank compiling (and passing tests) at each step of the way. So far, I have done that. We can keep our
visit_object
calls and replace them only as needed, for one behavior at a time. This also allows us to benchmark as we go.I will keep this PR up to date with
main
, but it'll get harder and harder to do as we replace more and more.