 Hello folks. Welcome back. I'm quite excited about this stream actually for a couple of different reasons The first one is that it's been a while since I did just like a pure live coding stream Like this won't be a crust of rust. This won't be as sort of Educational stream in the sense of the the crust of rust streams that I've done in the past Instead, this is one where like we have a programming problem Or we're just gonna like dive in and write a bunch of code and see if we can make things work The other reason that I'm excited is because this is a project that Like I'm kind of proud of like I think this is a really cool concurrency primitive and I'll explain it as we go a little bit and The other day I had this like realization that I could do something cooler with it again I will explain what that means and so I've been like excited for this stream for like several weeks now And so it didn't just barely lost the Twitter poll of which stream to do next I was a little sad and and why I immediately went to this one next so Here's what we're gonna do This stream is gonna be on a Project called Evie map and Evie map is something I built for I built for Norya for my PhD thesis work and Essentially, it is a is a concurrent hash map but it's a concurrent hash map that is heavily optimized for reads and It it Puts a lot more onus on the writer to do locking than on the readers and the readers basically get to be Completely lock-free They are the reads are very very fast. They scale very well. In fact, if you scroll down here I think there's a chart of like as you increase the number of readers the throughput for the readers just goes up linearly with it with the The number of readers No matter how many writers there are and that's a really cool property for a concurrency primitive But and I'll explain how it works in a second the downside of it is that This implementation was written very specifically for Norya's use case And you can see that if you start reading a little bit of the description of the data structure So it's a lock-free eventually consistent concurrent multi-value map, which is a mouthful the lock-free part is that there are no locks and readers in fact Never have to wait on writers at all. So it's like wait-free for reads But I don't want to use the term wait-free because I don't know whether I completely understand the like theoretical ramifications of that word But but pretty close to it Eventually consistent in the sense that the writer the writer can choose when to expose changes to the reads They don't become immediately Visible to readers. We'll talk about that in a second. It's concurrent So you can use it for multiple threads at the same time and in particular. It's concurrent for reads So there's only one right handle. There's only one writer if you wanted to have multiple writers They would have to serialize through a lock, but it's concurrent for reads So you can have many many readers and they don't contend with one another And it's multi-value So the idea here is that where a normal hash map is from key to value a multi-value map is from a key to a set or a bag of values and And that means that it has some kind of weird restrictions so I've had a number of people file like issues with it and and let me see if I can explain this a little bit more detail sort of before we dive into the technical things of how this works, so The idea with a multi-value map is that you can this is just sort of the same way that Standard hash maps work right you have an insert you have keys you have values But one difference is that because it's a multi-value map we can insert multiple values For the same key and now if you fetch out a given key You get back all the values for that key not just the latest one And here you also see this refresh operation right so we inserted these but then we did a get and that gets They'll return one for Grimm's fairy tales until we call refresh on the writer and at that point the reader actually sees that the changes that we made And there are a number of implications of doing the map this way Which is once you have a once you have the value bag Actually be a bag and not a single value You get into some complications because people need to be able to Remove a given value from the value set for a key And you can see this in if we look at a right handle for example You'll see that not only do we require that the key is hashable and can do equality. We're required that the value Implements eek and hash 2 and that's because if I want to remove a given value I need to be able to compare values so values need to implement eek And in order to store the value set efficiently we store the values themselves in a hash map Or actually technically in a hash bag and so the values also need to implement hash and this is This is pretty onerous like you might not think about this very often But usually when you use a hash map the the values are like arbitrary things like you might want to stick like Channels in there or something right the send half of a channel But if the send half of a channel is neither eek nor hash And so this this requirement is pretty sad And so this this kind of question comes up in a in a couple of different ways one is that Storing the value the value set as like a larger collection as opposed to a single element Means that we're sort of wasting memory for anyone who wants to use it only as a single value map Someone you want to use it just as a regular key value map And so it's been a bunch of questions of like how hard or easy would it be to move this to something that is Just what like key to value And similarly people had questions about why is there a hash and eek requirement? Is there a way that we can get rid of it or is there a way that we can get rid of it only in certain use cases? And the answer to that so far has sort of been no that if you wanted to remove the Multi-value part of this you would basically have to re-implement the whole thing and the whole thing is fairly complicated so it would be a little sad to have to keep a fork of this whole data structure just to support like a single key value map for example Which then brings me to The topic for today's stream so about two weeks ago I had this idea for how we might be able to make EV map generic over the collection that it is concurrent over So that way you can have sort of an EV map that holds Regular hash map or that holds a multi-value map or that holds a B tree map or something like it And we'll get into the details of this in a little bit But but first I want to explain how EV map actually works because we're going to dive pretty deep into the guts of it And it's important to have an idea for how these things fit together I'm also going to do some questions because I saw there were some questions that came in while I was discussing this On the topic of EV map itself so Let's see This is not based on flurry map So for those of you who may have seen some of my earlier videos about porting the concurrent hash map from Java to Rust That ended up with a Crate called flurry. This is not flurry This is a completely different data structure that I actually built long before I built flurry And the use case is fairly different. So flurry is built as a fully concurrent map where you have concurrent reads and writes EV map is specifically built for the case where you have many readers and you want as little overhead for readers as possible It is an epic based map sort of I'll explain that in a second and yes, this is the data structure that I described briefly in a talk I gave like Year and a half ago or something maybe two years ago now on Writing a concurrent database in Rust and I'll link that video when I post the video on demand as well You know, let's see Is it conceptually a set of key value pairs? No, it's conceptually it's a map from a Key to a bag of values a bag differs from a set in that each value in the set can also have a Sort of multiplicity like a count so you can have multiple elements that are the same in a bag Will there be any performance improvements, um, I don't think this is gonna improve performance That seems unlikely It's hopefully it's gonna keep the same performance Does the EV and EV map stand for something? Yeah, it's in for eventual so the idea here is that The map is eventually consistent in the sense that only when the writer chooses to synchronize with the readers Are the results actually displayed the readers and that will be pretty clear Once I show you how it works behind the scenes If value requires hash, then how can you have multiple of the same? Well, I mean just because the value is hash doesn't mean that I you disallow duplicates, right? This is just a way to efficiently Store that bag of values so that you can efficiently remove something if you choose to Okay, so let me explain how EV map works and for that we're gonna have to do a little bit of drawing Let's see here so I think what we're gonna do here is start out with a nice blue color Sounds like that. All right So the the core idea in EV map is that we're gonna keep two maps Instead of keeping one map So we're gonna have one map over here and we're gonna have a copy of the map over here and If you imagine that all reads come from above and all writes come from below Then the the basic structure we're gonna have is that all the rights are gonna go to one map and all the reads are gonna go to the other map As long as this is the case, there's no synchronization needed Right the writer knows that it's the only writer that accesses this map And so it can just do as many rights as it wants The readers know that there's no one writing to their map So they don't have to synchronize at all they can just read directly from the map Great so far so good the challenge of course is what happens if you if the writer Has made a bunch of rights and wants to expose those rights to the reader currently the reader or readers will never see any other modifications that the writer makes so the way we're gonna do this is that This no this pointer up here with the readers this pointer here is going to be an atomic And the writer has a mechanism for atomically swapping that pointer to point over here instead Great so that part is pretty easy right? The writer just does all the modifications It wants to this map and then swaps over to the readers to this side and now the writer is gonna move over to this side Okay, that seems fine so far But now we run into two problems the first of these problems is that there might be some readers Who are still in the process of reading this map? So they read the the atomic pointer before we did the swap and so they're still stuck in the map on the left This means that the writer can't start modifying the map on the left because there might still be readers in it So the writer needs to have some mechanism for waiting for all the readers to depart And I'll explain that in a second the other thing is that the map on the left Doesn't have all of the modifications that the writer has already made in the past to the map on the right The map on the left is the old value of the map from the previous time that the writer did the swap So we're actually going to require that the writer keep an operational log Also often called just an op log And the op log is going to log every change that the writer makes so this might be something like insert Key value this might be remove Key this might be clear Who knows right? This is gonna keep a log of all the operations that made This also means that it needs to keep a copy of the key and value for each such operation And then when it moves to the other map, then it's gonna read out these values This this part of the op log and it's gonna reapply it to this map First and then it's gonna start doing its other operations and those other operations then are also gonna go to the op log right, so this is the way for the writer to Make the the stale map on the left Become equal to the map it just less for the readers so that at that point forward it can continue to just do normal operations Does that make sense so far? I know there's a there's like a lot of detail here, but let me Pause here before I explain how the writer knows that the readers have moved on As a reader can we choose to read and eventually consistent mode and atomic mode No, so the readers always go through whatever the current value of this pointer is So there's no there's no modes for the reader. It's just they read the pointer Whatever the current value is they go to that map and they read from that map That's all the reader has to do And that will be whatever the writer has chosen to reveal and this swap by the way we call a refresh I believe if you refresh every right, then it's basically fully consistent. Yeah, that's right If you call if you call refresh after every right, then readers will always see the latest right And so that is that is a way to use this data structure to provide fully consistent reads For every insert into the map you have to do two allocations one in the actual map and one in the op log Not quite so the operate the key and value is is given to us by the caller Right the caller does like an insert and that gives us an owned value of the the key and value in most cases at least Right, so we can just stick those in the op log and then we do need to clone them So we need to do one allocation for For sticking it into the the map proper Now you might also observe that once we apply this op log to the left side What we end up with is every key and every value will be present in both maps Right because it has to be that the two maps are copies of each other. So there is this trait called Shallow copy That is a part of EV map I wrote it as a part of EV map and it basically just has it has one unsafe method called shallow copy and the idea of shallow copy is to Copy a value without copying its allocation. So for example imagine that you have a box T right and you can make another box T by Taking the pointer out of this one and stuffing it into that one Now this is clearly unsafe, right because now you have two owners for the same heap allocated value But as long as you're careful about only dropping one of them and only dropping that one after the other one has been Has been forgotten and that there are no pointers to it anymore. This turns out to be safe So the idea is that if the value type here was box and this had a b1 and this has a view as and another instance of b1 so they both point to the same p-val occasion then The only thing we need to be really careful about reading both of these at the same time is fine Is that when b1 is removed from the map the first time? It's removed. We just forget it the second time. It's removed. We drop it Right so these two are different and we have to make sure that the second time We apply in a given operation is when we actually drop and the first time we don't actually drop And this can be fairly tricky to get right and you'll see some of that in the code later But it doesn't mean that we don't have to duplicate all the allocations Just keeping copies to grade performance it shouldn't because these still point to the same heap allocation While applying the updates from the stack our new updates blocked. Yes, they are so The there is only support for a single writer in eb map at a time You can't have multiple writers and the when the writer does a refresh that refresh swaps the pointer Applies wait for the readers to depart and I'll explain that in a second and applies the off-log So refresh won't return until then and refresh takes a mute self So no other operations can happen on the map although reads can still happen It's just that other rights can't happen. The writer never blocks reads in the scheme Is there a point where the writer has too many ops in the log and just chooses to clone the map over no The there it that might to be a reasonable optimization at some point But currently that hasn't really been something that's come up. No Especially it all it depends on is how often you refresh, right? So the caller has control over how long the off-log is if you refresh after every right Your off-log is only ever size one. And so it doesn't really matter And so if the if the client thinks that the off-log is too long, they can just choose to refresh What could go wrong with letting readers that started before the right operation still read from the old structure So the problem with the old readers the readers in blue here Is that we can't we can't start or the writer can't start to modify the left Map until all the readers have departed because otherwise you have a read-write data race, which is not okay Wouldn't an arc T also work? We could store we could wrap every value and every key in an arc But that would mean that we have to do an extra allocation for each operation Which is unnecessary like we can just alias the box In fact shallow copy is also implemented for arc because it's it's trivial to shallow copy an arc Is the off-log bounded the off-log is bounded by when you call refresh, but it's not bounded internally Could this work with more than one writer So the way you do this with more than one writer is you have them you have a so the read handle type is Basically clone like you can get many of them. The right handle type is not And so you would stick the right handle type in a mutex that way you can have multiple writers If you actually want to concurrent writers, I don't have a scheme that works for that, but it might be possible What's the condition the writer chooses to do the refresh the writer does a refresh Whenever they decide to so if they want strong consistency, they swap after or the refresh after every write Otherwise they just refresh Sort of infrequently think of it as batching updates They choose how often they want to batch or how long the batches should be Could the off-log just store what keys are dirty and then update the old map from the new one it could but That also gets pretty bad especially if you have something like a multi-value map where You might have a value set that has like a thousand elements in it If you insert one element you don't really want to copy a thousand elements in the map Like delete the old key like the 999 keys The 999 element value set for the key and then copy over a thousand elements You'd rather just do the insert there might be cases where it's cheaper to just Have the off-log just record read from the other map It gets really complicated though because the off-log remember if the off-log have as many entries in them You sort of need to do those operations in order, right? So imagine that there's an operation that's like an insert followed by a clear, right? You can't do the insert by reading from the other map because the other map also has a clear operation applied Which if you did the clear off, which means that its value set is empty. So It's not quite that straightforward In theory, maybe you could map like all these keys are dirty At the end of the off-log and then just copy those over But then you potentially end up doing a lot more work than the new technically need to Do you really have a box there if the pointer is not unique? Yes, we're aliasing the box So that it's definitely unsafe code But it's okay as long as you only ever access it As a read only and you only drop it once and there's no use after free Let's see Doesn't have to box T to the same allocation break aliasing internally. They have unique T It's fine as long as you don't use them mutably, which we don't Do all what about iterators do all iterators have to be dropped before the old copies considered without readers. Yes So the when a read and this is a good sort of transition to epics When a read reads this atomic pointer and say it reads the old value like before the refresh It now has a pointer Into this map like it knows of the the Let's go back to the blue here, right? So a reader that reads out this map and is now operating on this map It is in that map until it No longer accesses that map, right? If it has an iterator and it walks that iterator It's still holding up that that map if the writer were to just like Go ahead and do its rights. It would be concurrently modifying what the reader is currently reading, which is not okay And so and so this is this would not be okay And in fact any operation that the reader continues to do on this map must hold up the writer It's only when the reader eventually Finishes this operation in returns and then reads the atomic pointer again It's only at that point that we know that the writer can can keep going Is it worth trying to optimize the log by looking at clears and removes before executing the inserts? Maybe I don't think it's worthwhile. I don't think that Optimizing the Oplog makes a difference here. Remember this data structure is written for when you have a lot of reads The Oplog is free as far as that is concerned because it's a right operation It's true that maybe you could speed up writes a little but it wouldn't affect reads and we care only about reads in this case And in the in this stream, I will not be focusing on the Oplog I mean it's gonna be a part of the implementation. We're gonna do but we're not gonna try to optimize the Oplog Are you planning exposing shallow copy is his own crate it doesn't look like it's currently available outside even map No, I don't want to expose shallow copy because it's like it is a very dirty hack that Requires extreme care to use correctly. So I don't think it's a good idea for other people to use it Our box would work. Yeah, I mean you could you could use an arc to De-duplicate these values as well But it would require both an extra pointer indirection and an extra heap allocation for every key and value Which is just not worth it Okay, so now the question becomes How does the writer know when there are no readers left in the map and for that we're gonna start a new drawing So we're gonna have let's say here reader one reader two three and reader four and we're gonna have the writer down here and What we're gonna do is every reader It's gonna have an epic counter and This epic counter is not global like many epic counter schemes are global. This one is not Every reader is gonna have an epic counter, which you can think of it's really just a number But I'm gonna write it Okay, fine, we'll do it simply first so like seven nine Ten and this one is at seventy four So these are just the local epic counters for each for each reader and the writer is gonna have a Pointer to every epic counter So it it has a way to get at every reader's epic counter So here's what the the rule is gonna be for how readers update their their epic counter When When a reader wants to do a read So just before it reads the atomic pointer, it's going to increment its epic counter Then it's gonna do all the it's gonna read the atomic pointer It's gonna do all the operations at once on whatever map it it gets back and then it's gonna increment its epic counter again So this means that a reader that is inactive or is outside the map Is gonna have an even epic counter number, right? Because it's a double increment an increment on enter and an increment on exit But crucially these increments are local increments, right? They increment only their epic counter and that value is not normally shared It's not shared with any other reader and the writer will only be accessing it if they're if it's trying to do a refresh So this means that in general if you think about interested like CPU cache coherence that epic counter cache line is gonna be In exclusive mode for the CPU that's running that reader And this means that the the even though it is an atomic update of a number It's a sort of CPU local atomic update in general the CPU will already have the lock on that particular cache line So it should be very fast and then the what the writer is gonna do is the writer is going to one Swap pointer to Read all Epics and Then three Read all Odd epics Again until They change Okay, so this is this part is a little weird So the writer is gonna swap the pointer, right and there's gonna read every epic counter And when it reads every epic counter It's either gonna see that that value is even or it's gonna see that that value is odd If the value is even That means that the reader at the time when it did the when the writer read the epic the reader was inactive So that means that it the reader cannot be in the old map If the reader was to immediately after do an operation It would read the atomic pointer, but we've already done the swap So it must be looking at the new map instead So any epic counter that we see is even we know that that reader is sort of safe Like we know that it has it is no longer in the old map It might be idle or it might be in the new map of both of those are fine any epic counter that's odd Might mean that that reader is still in the old map And so we need to continue to sort of recheck To to detect when the reader has moved on well when has the reader moved on whenever their epic counter increases We know that they must have left the old map and it might increase by a lot Where the readers might be really fast and might be faster than how often the the writer rechecks his value If the value is incremented by one that's still enough because it means that if it if it was odd And it's incremented by one that means it's now even which means that it's now no longer Accessing the old map and therefore it's safe for us to use it So the algorithm here is actually fairly straightforward for when it's safe for the writer to continue Does that roughly makes sense so far? Does it do anything to handle starvation of writers? There's no lock here the only well, so The answer to your question is no the If a reader just continues to hold on to the like imagine a reader takes out an iterator to the old map And then just like walks that iterator really slowly or just holds on to it, but doesn't drop it Then yes, that will hold up the writer indefinitely, but as long as readers are As long as readers are doing things the writer will get to go Right because remember the writers the right and read here are Completely independent right if a if a read if a reader does a read and then there's another read That's second read might come from the new map at which point the the writer can continue If What if the counter rolls over The if the counter rolls over that's something that Evie map doesn't handle, but it's a u64 counter So it seems highly unlikely in theory there might be a way to in theory I could make the It might be safe for the readers to like Reset when they hit even or something, but it's not Quite that simple because the writer is waiting for a value that's higher But yeah, it's a little unclear You run into a Potentially really bad case where the writer the writer continues to see the reader as being odd But the value starts going down and the writer gets very confused So I don't I don't think rollover is really a problem in the sense that rollover just doesn't really happen in use 64s Do you have any more use of the epic apart from signaling to the writer? If not, why don't we keep the values to 0 and 1 it can't be 0 and 1 and the reason for this is Imagine that we swap the pointer Then we read all the epics and we find that some readers count is one Then we read their epic again and their counter is one then we read the epic again Their counter is one the reader might have moved on from the old map to the new map But we can't tell because we only have the binary yes or no The fact that the epics keep increasing means that we can we can sort of figure out what round the reader is on And this is important because if they're on Another iteration like if if they have read the pointer again since last time you checked Then it's safe for the writer to move forward and you don't get that with zero and one The counters are all atomics. Yeah Yeah, so there are a bunch of people asking like can't you just have this be a yes No flag and no you can't or rather if you did the writer will end up waiting potentially a very long time Before they can move forward because they might always see that the reader is in like a pending state And then it doesn't know that it's safe to move forward because it can't distinguish between the reader has Wasn't pending went to idle and went to pending again It's the ABA problem like you don't know whether the reader has moved on or not So you need to be pessimistic about it whereas with the incrementing epics Then you always know whether or not the reader has moved on Can we assume that the readers can be from different threads yep the read handles are different threads Can the writer check if the readers counter is different instead of greater Maybe maybe Yeah, that might be a way to fix rollover that the writer just waits for the epic to not be the same as it was You might be right that might be sufficient How can the readers write their epics and the writer read the epics without locks so atomic counters that you do this Instead of a counter could you not have a unique ID for each of the two maps for the readers to supply to the writer like the reader said I'm on map zero You could do that but the readers would need to So the way you would have to do that is the readers would store like which pointer They're at probably rather than zero and one because there's no left and right in in memory, right? But I guess they could probably store the pointer of their current map. I don't think it makes a difference. I Think these are about the same Yeah, that could probably be that could be fine Yeah, yeah, so I think it could be a pointer instead of a counter one thing that's nice about this is Yeah, no, I think you're right. I think it could be a pointer I Feels like there'd be a race if you store the map pointer, so you'd have to read it and then store it I don't think that's a problem Because as long as you use the pointer you read I think it's fine. Oh Yeah, no, you're right. You might read the okay, so this is the reason you can't use the pointer that's a good good observation so The proposed scheme is that every reader instead of storing an epic counter stores the pointer of the map that they're accessing Imagine that the following scenario happens reader reads the atomic pointer and Then it needs to read it in order to write it into its sort of local storage, right? But there's a gap in between those two operations like imagine the reader reads the pointer and then the writer Squeezes in in between before it writes out the pointer of the map that it's using and the writer sees the old value for the pointer and Goes oh, this one is on the map that this one is already on the new map So it's safe for me to proceed and so the writer continues to do rights assuming that the reader is on the old map But then the readers stores the new map pointer in there, but it's too late because the writer has already moved on So this is the reason it can't be the pointer Nice good catch All right, so now that we have the scheme in mind There's also an optimization that eb map makes which is that it It turns out you can be a so that the How to explain this um, I need a color green or whatever. Okay, so This step is super fast, right? This is just an atomic pointer swap This one is also super fast because you're just reading all the epics this one though might take a really long time because It might be that there's some reader that's like really slow and hasn't moved on And every time you call refresh you sort of have to do all these three steps And this last step taking a long time means that your fresh is going to take a long time so it Turns out that you can be a little bit smarter about this and you can have that wait time overlap with when You can have that that that wait time overlap with the code between different refreshes So the idea is to have refresh return here And then only apply writes at the beginning of the next refresh So we're gonna add a step zero which is apply writes And now This is gonna be the first this is this whole thing is gonna be the first step of the next call to refresh And what we're gonna do is we're gonna have whenever you do an operation on a writer It's not actually gonna modify that the right map The one that we currently have a right handle to instead. It's only gonna push to the op log So if you do an insert it will not modify the map it will just add to the op log and nothing else And this means that now it's actually safe for us to return from refresh right here Because We were not actually gonna modify the map While we're waiting Because in the interim after coloring fresh all the writes we do are just gonna go to the op log anyway And then the next time refresh is called then we're gonna do this wait first and the at that point Almost all the writers are They're like almost guaranteed to have moved on because it might have been a really long time between the calls to refresh So we're almost guaranteed that all the epic counters have have either incremented or become or become even And so that means that now every call to refresh is actually fast because the right that this sort of the minus one Fourth step, which is this one, right? This one is likely to be fast because it's been a really long time since we read all the epics This one is gonna be pretty fast because it's just applying a bunch of rights to something that we're the only ones modifying And then we swap the pointer and then we read all the epics So we're just sort of offsetting the cycle a little bit so that we can Overlap the waiting with the writer doing other operations that they do between calls to refresh So that optimizations seem roughly right So with that optimization we can no longer read through the writer that's correct The writer that you cannot read from the right handle to see what the current state of the map is you could So in theory you could do this by doing a read from the map and then walking the Oplog to figure out Walk the Oplog and like apply them to your return value But it gets pretty complicated, but it does make refreshes a lot faster. So it's definitely a trade-off but in general the writer is unlikely to Are these in my experience the writer is unlikely to really care about the reads Think of it as all reads regardless of whether they come from the read handle the right handle go to the read map Does this mean that on average the Oplog is twice the length as one refresh cycle Yeah, so the Oplog has to hold this actually means that the Oplog is a little bit more complicated the Oplog holds both Values we haven't applied to the right map and values we haven't applied to the read map So the Oplog has sort of this like Watermark in it of everything up to here you've applied to the right map and not the read map and everything beyond this Watermark you've applied to neither map All right, great so now that you have an understanding of How EV map fixed together. I think it's time to look at some code And I'll try to explain actually what we're gonna do is we're gonna go back to this suggestion I had which is Currently EV map is is hard-coded to deal with multi-value maps like the Oplog is Like the values of the Oplog are an enum called operation and it has operations like insert delete Empty which is for removing all the values for a key Set which is to replace the value set rather than append to it and A couple of other operations that are there are less interesting It's hard-coded to like have both things be a map. It's hard-coded to the the value The values of the map are of type like value set or value bag and That's kind of sad because if you if you think back to this this design There's nothing about this design that's map specific, right? You can have an Oplog over all sorts of structures. You can have an Oplog over a vector You can have an Oplog over a hash set. You can have an Oplog over a B-tree map You can have an Oplog over whatever your own data structure is And so the and the sort of the epic scheme and the choosing when to use which of the two collections That part is entirely data structure agnostic and so it's kind of sad for the sort of at least in my opinion coolness of the EV map sort of concurrency primitive Being limited only to the one implementation that I wrote I would much rather have this be sort of a Rapper concurrency primitive that you could then use for your own data structure So the question becomes how can we do that well The That's basically the the idea I had two weeks ago and sort of in hindsight This is very obvious, but early on there wasn't I didn't have a good idea for how this might work So the basic idea here is that we're gonna have a new type That's gonna be basically a concurrency primitive think like mutex except it's It has very different semantics I'm calling it left right here for two reasons One is because there's a left map and a right map, but the other is because there was a There was an announcement back in like I Want to say like 2013 or something from someone who's like we've invented a left right concurrency primitive and the the white paper that put out was was very short It was just like a brief description of here's roughly what it does and it looks a lot like EV map And this is before I built the EV map I didn't find out about this until a couple of years ago like after I built the EV map and I think what they were going for was the same thing as EV map is But but I don't think they've ever published the actual algorithm But sort of in honor of them being first I figured let's call it left right And so the left right type is gonna be Generic over two types T and O T is gonna be the type of the left and right things So this would be sort of the hash map key and value bag V for EV map for today And oh, which is gonna be the op log type. That's the enum that we're currently using an EV map today And then we're gonna have a trait absorb and absorb is Gonna be generic over O the op log type and the idea is that it's gonna require that the T the thing That we're sharing implements that trait for the given O And the trait is apply first and apply second apply first is The first time you apply a thing from the op log So this is when you apply it to the right map and apply second is the second time you apply a given thing from the op log Which is when you apply it to the read map These need to be These need to be different for a couple of reasons the first one is that when you apply it the first time You don't own the value in the op log because we need the value to remain in the op log so that we can apply it again And so you we can only pass an immutable reference to the the op log element We can't actually give you the element so it might have to clone or something or use something like shallow clone in order to stick it into the The right map and then apply second we can give in the O And the the other reason why these have to be separate is that if we're using something like shallow copy And you're doing something like erasing a value or a key if that value was shallow copied Then the first time it's removed We need to forget it instead of dropping it and the second time it's removed We need to actually drop it and so the implementation needs to be able to distinguish between these two cases And if we have this kind of primitive Then ev map as it stands today could just be expressed as left right with a hash map of key in a hash and a value bag and an operation where the operations are like a hash map operation of key value And what's neat about this is that this if we had this kind of structure It'd be trivial to use left right to implement other kinds of collections in an efficient way So for example, if you wanted a hash map that was just key value You would just have a left right hash map key value and the operation type would be something that's operations over a single single value map Now there are some other aspects to this like It it means that we can't have quite we we need like a layer of abstraction in avm app to make Ergonomic access to the to the underlying map nicer because with the left right right handle All it could really do is have like a penned operation and refresh Because it doesn't know about things like inserts it can't because it's supposed to be generic So the idea is that ev map would have to also provide like a wrapper type around right handle That has the hash map specific implementations of these methods like it would have an actual insert method That just ends up calling append op on the inner right handle All right, does that does the the plan here roughly makes sense Wouldn't you need a generic type constructor to make this work like for the same reason there's currently no collection trait I don't think so because left right doesn't actually require that the Thing you get is a collection like it doesn't require that the thing is a map all it requires is that it can apply things from the op log So I don't think you need generic type constructors for this Although like it could be that we start coding this and then we're like oh damn this there's no way we can do this But but I don't think it should require that and that it's specifically because we don't We're not trying to proxy any particular methods of T All we're gonna do is like a a D ref into Entity like this is a wrapper type not a generic trait Could operations be closures rather than an enum Maybe I think that would be very hard to work with because you still need something like apply first or apply second I guess you could have like the Entries in the op log are closures that you call on the map and They're like past the value of what this is the first or the second it would make the the type specification really awkward because You presumably want different closures for different kinds of operations Which would in turn mean that your Op log type would be like boxed in Fn mute or something I mean it could work. I don't think it's nicer Collection seems like the natural use case here might be handy to have a non collection type to keep in mind during implementation What might that type be? Yeah, I again, I don't I don't think this is specific to collections But it is true that all the examples I have in mind are collections all of what I've written so far is entirely collection or Type independent right so apply first and apply second is just you need to be able to whatever type you have Needs to be able to be modified by an operation For that can be stored in an op log, which is not specific to collections The only thing we're going to expose on right handle is append an operation to the op log And the semantics of that operation you get to define yourself refresh which is just Now expose the changes Which is also not collection specific and then the last thing we're going to do is really just implement D ref for The read handle and the right handle into the inner type So I don't think any of this is written to be collection specific in terms of having a non collection type in mind It might be that That's a good question I can't immediately think of a non collection type you would use here But then again, I've been wrong before yeah, I can't immediately think of a one But but looking at the API at least it doesn't it doesn't feel like there's anything collection specific in it Yeah, I mean, okay, so it's a good point from comments to like maybe a counter Or a collection of counters, but like you could maybe have a counter a counter is a really weird example because Like why wouldn't you just use atomics? instead But but okay, let's let's take a counter as a sort of strawman example, right? Apply first and apply second would just be applied the the modification to the number that you're storing Right handle would either refresh the count or append an operation that you're going to do to the count And you could D ref into the count whenever you wanted to so this would work for counter, which is not a collection Well strings are collections, right a string is just a vector of characters really Yeah, like a very very complicated way to do atomic counters Okay, so let's start to actually look at the code here So Evie map is Currently entirely geared around this this idea of supporting maps And so I think what we're actually going to want to do here is Create a new crate that holds this sort of generic version and then have Evie map wrap that type And and cargo has a good feature for doing this already of sort of bundling things together, which is workspaces So I think what we're actually going to do is we're going to create We're going to turn Evie map into a workspace that has that type and Evie map Where Evie map depends on that type So let me go ahead and start a new here and call it left right And Now I need to remember how this works So we're going to do we're going to make dear left right and Actually, we're going to not do that. We're going to cargo new lib left right And then we're going to make dear Evie map and we're going to get move cargo tumble and Build RS and source and tests and Benchmark Into Evie map. Oh Why do I have a build RS in here? Oh, that's from Something else Let's just move that out for the time being What else do we have? Why is it complaining? Right? All right, so we moved that There's also Evie map derive so Evie map derive is a way to derive Shallow copy safely for types. I forget exactly how this is specified at the moment I think it's currently just like a sub directories. If you look at Evie map cargo tumble I think it has a path dependency, maybe Oh, it's actually just a separate sub directory. Okay, so this is going to be even nicer So we're going to create a cargo tumble workspace Why always forget what the specification for this is Cargo manifest format and I specifically want workspaces Mm-hmm Yeah, so what we want is a virtual manifest We don't sort of have a primary crate really anymore because it's not clear whether left right would be the primary crate or Evie map would be the primary crate So I think we're just going to have them be separate and then we're going to say members That was the thing I wanted to look up members are going to be Evie map Evie map derive and Left right Let's just do a cargo check here to see if this actually builds Yeah, I can't it would be a good like minimal unit test is true Fail to get Evie map as dependency of package Evie map drive Drive cargo tumble See that now builds That does not build Oh, I really don't want to deal with Evie map derive right now. It's just going to be I'm just going to ignore it for now And fix that up later Evie map derive is like it's not as important for us to fix this and it's certainly not important for the stream Great. So Evie map itself builds. I guess I should fix that build error Broken introduction links. So it's called now So Great, all right, so now we can start I guess I can Commit this now. I won't do that yet All right So now we can start sort of from from something that's conceptually just an empty project and then start to add from here and What we really want to do is we sort of want to start from what Evie map has right because all of the A lot of the types are going to be the same a lot of the the concurrency mechanism is going to be the same So we're actually just going to copy everything from Evie map source into here And then a lot of these things we're really just going to be removing stuff So we can get rid of values. That's not going to be important Oh, I did a stupid deny. I Should have moved that into source So we're going to move inner a lib shallow copy and write Into source and we're also going to move read into source great source lib So Evie maps sort of root root type or root File doesn't really do much it mostly just Forwards into types and various modules. So most of this is just going to be cleanup Epics we're going to end up keeping predicate. It's not going to be used Operation is no longer going to be used in here We are going to have right handle and we're going to have read handle and read guard and Maybe read handle factory. I'll explain these in a second We're not going to have shallow copy And we're not going to have the sort of builder for a map because that's not actually important anymore We are going to have this This We're not even gonna have that I don't think we are gonna have a new and New is going to do what that constructor used to do And of course, we're gonna have to change the generics here to match like having the taking the type and the operation We're not going to require anything of V anymore because we're not even going to have a V Okay, so there's one thing I haven't really talked about which is meta meta is Mostly uninteresting the idea is that Sometimes you want to communicate to the readers What version they're on sort of so you can think of this is like an additional little bit of state You want to include with every swap and meta is that information? So the idea is that Rather than have the pointer be directly to the map We're just gonna have that we're gonna have the pointer be to a struct that we control and it holds the map But it also holds this like extra little bit of meta information Now now that this is generic I don't think we need to meta anymore because the user can choose to have something that's like a map and some meta information So I think maybe I Think maybe that can now go away So we're gonna get rid of meta. It's gonna make things a lot nicer Right, so new as we talked about before it's gonna take a t and an o And it's gonna return a read handle to And what we're gonna require is that t implements absorb o and We are missing the trait absorb So we're gonna have to write that and that has an absorb first Which gets a mute self and a mute oh and An absorb second Which gets the actual low So new is gonna create a new epics epics here You'll notice is the it's a an arc mutex slab of arch atomic use size Which is quite the mouthball, but basically it is The writer has a has access to a list of pointers to Epic counters so this inner arch atomic use size that is the epic counter for each individual readers a slab you can think of as Sort of like a you can think of it as a vector, but where we can remove specific elements efficiently and we can index them with consistent keys We won't be talking too much about slab you can mostly ignore it think of it as a list And it's gonna be an arc mutex and the reason that we have to arc mutex this even though Generally, it's only gonna be the writer that acts as this It's an arc mutex so that you can clone a read handle Right, so if you have a read handle and you want to construct another read handle from that read handle Basically, you want to clone that read handle the read handle needs to Store its epic counter, right? It's gonna allocate a new epic counter It has to store that somewhere where the writer can get access to it And so it needs to be able to modify this And that might happen concurrently with the right, so that's why this has to be an arc mutex. I guess I should add slab to I should actually add a bunch of these but Specifically slab it's the one we're gonna want here All right We don't need any of this capacity business inner is a type that Inner is the thing that gets pointed to by the atomic pointer So if we go to the inner file a Lot of this can be simplified to you see that it holds data which is You Basically a map There's a feature in EVM up that lets you choose what map implementation to use But it basically just holds the map it holds the meta and it holds this ready flag The ready flag we might still need So the ready flag is there so that If you try to read After the I forget what I'm using ready for even ready Oh it's Yeah, this is also something that the user can choose to implement Ready is just used so that if a reader tries to read before the writer has had a chance to write anything then it gets an error But I don't think we care about that here Yeah, I don't think we care about that here. So in fact the whole inner type just isn't needed anymore So we can just do get RM source. Oh, I need to see the into left inner Just RM source inner That's nice. I'm that makes me happy So inner goes away So really this is gonna give us a read handle T and we're also gonna require the T as clone And we're actually gonna take in the Original T I think So the idea is that we're gonna take in some initial collection. We're gonna clone it to get two halves of it We might not require a clone. We could take a T1 and a T2 So actually we might provide a second constructor here that takes them explicitly All right, so we can do this right and then we can have a second Constructor that's like new with clone which takes a T that is clone only takes one T and then Just clones to get the two halves You could even have one that doesn't take a T at all, but takes a T that implements That implements default and just gives you an empty one I increased the font size just a little bit. Yes, I can it's weird So my screen is 4k. So I'm showing you only a subset of my screen and then it gets scaled to 1080 so it's hard for me to estimate what the actual font size you end up seeing is I Be worried about a T1 T2 constructor not being marked unsafe because there's no guarantee. They're the same That's true. This does require that they are the same All right, let's go with this then. I Mean even if you take clone, you're not guaranteed that they're the same They might have different hashing algorithms, for example, but what's neat is that it that doesn't matter if you have like hash map for example can be used with EV map and it's fine because The right operations you do have the same effect regardless unless you already use unsafe APIs like accessing things by hash value or something Basically so EV map does require that the or that this primitive record does require that the Operational log is deterministic over the T All right, so what are we gonna do here? We're gonna have T dot clone and And T Right, so we're gonna give one clone of the T to the first read handle and one clone of the T to the first to the right handle That might make a good doc comment. Yeah, you're right. So here's what we're gonna do Notes this is a note to self like a note to developer A Document that operations must be deterministic I think there already is a document to this effect in EV map itself But I think it's like an internal comment or something All right, so this alone just simplified this file a lot I'm gonna go ahead and Remove all this documentation because it's map specific And I don't want it there and I'm gonna for the time being removed the warning for missing docs Don't need this don't need format don't need build hasher Great, so this file is already a lot simpler that makes me makes me very happy. So now we have Read and write and I think let's start with right because right is where most of the complexity comes from So we're gonna want to pull in the absorb trait, but that's probably all we need from super There's no longer an inner We do need read handle. There's no longer of values There's no longer any of this hash stuff So manually drop here is kind of interesting The idea behind manually drop is that we'd like to err on the side of caution like Remember how I said if we use something like shallow copy, you want to make sure that the first time you remove a value You forget it the second time you drop it by wrapping the value and manually drop What we end up with is that the standard case if we don't do anything special is that they will both be forgotten Which is a memory leak, but it's not memory on safety So so we do wrap them in manually drop However that I don't think that's gonna come up here because we're not gonna use shallow copy at all That's a decision of the implementer of this data structure So I think manually drop can probably go away the atomics. We do need arc and mutics guard. We probably need Don't know whether we need format, but We'll find out These stocks can go away right handle now is going to take T and O and again, we're and we don't even need to specify the The bounds here because we might as well do those Just on the refresh operation because that's the only one where we actually care that tea can absorb Oh, so this is a pattern. You'll see sometimes in rust where Some people put the trait bounds for a type on the type itself and then on every input block the idea being that You can't use this type if you don't satisfy these bounds So we're not even gonna let you construct them Whereas if you look at the standard library, for example, it's much more common for the bounds to be on particular methods So you'll be able to construct a right handle that has say Where T doesn't implement absorb. Oh, you just won't be able to do anything with it And there are a couple of reasons for that one is historical Where So there's a feature called implied bounds that I don't know whether it's landed yet I don't think so so implied bounds is that if you have where bounds on a type Then every input block for that type will automatically have those bounds included So you don't need to write them out But without that feature if you had like like you saw here, right a long list of where conditions for this type You have to repeat that list of where classes for every input block Even though not necessarily every input block relies on all these properties But you sort of made this statement that like this whole type doesn't make sense without all these bounds and now every input block Needs to repeat them and so by not putting the bounce on the struct and instead only putting bounce on the individual methods You sort of avoid this problem having to repeat yourself The other reason why you might want it on the type is is sort of related Which is imagine that someone else wrote a struct that contains a right handle if I have these bounds be on my right Handle I'm requiring them to put the same bounds on their wrapper struct because they can't have they can't name a right Handle without ensuring that these properties are are held if I don't include them here and only have them on the input block They will not have to include it on their struct and therefore everywhere in their code base So in general, I think err on the side of having them on the methods instead The reason this didn't is I think cuz index map at least which is one of the ev map backends And hash bag both required both had these bounds on the struct and that's why we have to repeat them here Accessing entries by hash is not unsafe though. Oh, yeah, you're right. They're like raw entry API I guess is not unsafe Because as long as you guarantee that the hash you get as you yeah, because the hashes are stored internally in the the handles you get back Yeah, so I guess really it's unclear where this unsafety should be It could be though the methods on absorb should be unsafe to be honest Not quite sure yet. We can look at that in a second All right, so the right handle is going to hold a box of T An Oplog, which is going to be a Vek of oh Swap index which is remember how I mentioned that the Oplog has his watermark for everything before or everything on one side are the things that Haven't been applied to the right handle or the read handle and everything on the other side are things that have been applied to the right Handle, but not to the read handle. The swap index is that watermark The writer has a read handle and the reason it needs to have a read handle is so that it has a way to Get a pointer to what the read map currently is So this this is the right map and it gets the read map through here And this is also what it uses to allow you to read through the right handle This is gonna hold the T in an O last epics is Remember how we read all the epics at the end of refresh and then we're gonna read again at the start of refresh and compare them Last epics is what we read at the end of the last refresh Meta we don't need anymore First and second are kind of weird First and second are optimizations where the first time you do a refresh You don't really need to use the Oplog The first time you do a refresh you can just clone the whole map But that does rely on the type being clone so it depends on whether we want to require The T is clone everywhere here whether we only want to require it in the constructor. I Think we're probably gonna get rid of first and second I Don't think they're optimizations that we want to generalize And those you'll see there's a field here called refreshes, which is only enabled during testing And this is so that we can test how many refreshes happened Like when we're writing a test we can ensure that it did the the number of refreshes we expect Yeah, it could be the absorb should be an unsafe trait Epics are you 64, but they're U size. Yeah, we should probably fix that straight away This should be an atomic you 64 All right, so we're gonna implement debug for to Where T it's so nice to get rid of all these bounds And I think the Oplog also needs to implement debug Right handle off log swap index our handle. There's no meta and there's no first and second anymore Great Creating a new right handle now is also pretty straightforward. The right handle is really just gonna be a T Takes an epics takes a read handle to and gives you a right handle to We're not actually gonna require anything of new here Don't need the meta We have to box the T because we're gonna use it in like an atomic so we need to have a pointer to it Which means that it it doesn't need to be heap allocated But the alternative would be that we need to like Take something that can be used as a pointer and it's easier to just box it internally And notice that this boxing only happens once Implement draw what's it implement draw? I forget Here we probably require the T implements absorb I'll show that in a second why that is So When we drop a right handle we want to Remember how we don't necessarily know whether it's safe to drop or forget a value And so when we drop a right handle, we really want to like Walk through the Oplog and then do the refreshes so that we can reuse the logic from there to figure out Which things are safe to drop and which things we should forget And so that's basically what's going on here this code I think can stay exactly the same as it currently is This There's no data anymore in W handle, I don't think Yep, that can just be a pointer to inside the W handle So this is going to be As D ref mute unwrap. So this now is a pointer to the thing that the box holds Recalling when to add bounce directly to a structure does it make sense to add them if they're needed and drop I think the compiler actually requires that that's a good point So I think it's actually going to require that we add this bound here Because otherwise, we're not allowed to use it and drop It's a good catch. It's a little sad, but you know Oh Clear is going to be interesting Hmm Not quite sure what to do about this one yet So the the trick we're pulling here is that in drop we make sure that the two maps are identical And then we want to Basically erase everything from the first handle before we drop the second but if we That's not gonna be nice So I think we do actually we are gonna want where is manually drop again, I Thought it was sync But maybe I'm wrong You Where is my oh man, of course, it's a man. What am I thinking? We're gonna want this I think to be manually drop And the reason for that is if there's like a panic or something and the right handle gets dropped It's not safe to drop what's in the right handle Or I guess actually yeah because there might still be readers in it But this one I'm not entirely sure how we're gonna do like we need some way to instruct the the T to Like remove all the elements, but don't drop them. This might have to be like a Like we're gonna require that the operations implement like a clear operation or something. I have to look at that in a second This one cast that's back into a T And Where do we get the R handle from? Yeah, that's fine You know all Presumably instead of boxing you can use some unsafe furry around pointers inside the right handle and the inner part of the reed handle But I think the boxing is way less painful and so it's not an optimizing I consider unless those allocations ever showed up as pain. Yeah the The other reason I want to do it with box is because it might be a stack value and Then you need to box it like you can't have them share a pointer to a stack value. It wouldn't be safe Because the pointer has to be stable over time for this to work. Let's see So To for right handle this is now going to require absorb. Oh on every input block So Right handle holds a unique pointer to the right collection and right handle is unique. Yes, right handle Well, it's not a unique pointer because there might still be readers in it It will be unique by the time we use it as mutable But right handle is unique. Yes Okay, so weight is a sort of helper function in right handle and what weight does is Basically, it does the weighting. This is specifically the step where you've read the epics before and now you're Waiting for all the epics to have iterated over. I guess this now has to be you 64 Yeah, and this is just code to like read all the epics I Lied a little bit when I talked about even an odd in practice. It actually uses the high bit I forget why it uses the high bit. There might not be a good reason And then it tries to figure out like if the If a counter has if an epic hasn't moved since last time we looked at it What do we do? Well, we can just retry like Imagine that there are like three readers or something that still haven't changed We can just optimistically retry a couple of times because reads in general are pretty fast But if it takes longer than that, we might actually want to yield the thread to give the readers a chance to run And so notice that weight didn't have to change at all. There's nothing map specific in weight And similarly for refresh Remove the docs because it's too high of a risk that there's something in there about maps So they should be rewritten anyway Yeah, so it calls so this is where you'll notice that when you call refresh it first waits This is that sort of moving the thing up to the to the top And this is gonna be a stirruf mute We're not gonna have the second optimization. Why is there? Why do we have the our hasher? That's interesting, I don't think we care about the hasher Oh I remember so there's a the hasher here is actually kind of interesting We need to make sure that when we apply operations We use the hasher of the map that we're modifying not the hasher of the other map And this gets a little complicated at times to keep track of But I don't think it'll matter here This is making sure that we apply the The corrects portion of the Oplog Apply a second We're not gonna pass in the hasher. Oh I see what's going on here This might actually be a little hard to abstract away so When Pretty sure this is wrong Why is it using the same hasher for all the operations that doesn't seem right I think something's wrong here But Yeah, so the thought I had was The the T when it is told to apply the operation second or first doesn't really know whether it's applying it to the Left or the right map although it should use the hasher from the appropriate map. So why is this a problem? It's a problem because Why is it a problem I Think you need to use the same hasher otherwise operations don't end up being a deterministic But I forget why I'm gonna remove this for now And then we're gonna come back to that later. I think could you give each of apply first on apply second a Reference T of the other collection so we can extract hashers if it needs to yeah, that might be the way to go about this Sorry you said I read it as a U size Oh, yeah, you're right. There's probably also the means that last epics Should be a Vek of you 64's But but but but but Where were we? Yeah, I think it would be nice to provide a handle to the other thing too Why this is to drop I forget what do drop even does it has been a while since I looked at some of this code Oh, I remember what this does. Oh, that's so sneaky um Okay, so This is a modification. I made a while back, which was instead of trying to remember That's crazy instead of trying to remember Whether or not it's safe to drop values. I just because manually drop is Is represented in memory the same way the inner type is I just cast between the two So if you're supposed to drop things I cast to then the thing the the version of the type that doesn't have manually drop for the values And if you're applying first then I don't cast to that type Now that Cast can actually happen Inside the implementation of apply second and apply first So let me see do I even call apply second anywhere else? Yeah, I didn't think so Okay, so I think this Trick is something that can be performed inside the implementer. So that is just going to go back to be w handle But I do think you're right that we probably want to pass in the Handle to the other thing So let me pull that back here So this instead of being our hasher is going to be our handle handle and This instead of being hasher is going to be I guess Just inner probably Yeah, it's going to be inner dot Load Do I really not use ordering in this type? Oh atomic fine Yeah, notice the comment here that because here we're reading out of the read handle We are now a reader so we can't swap while holding this because it wouldn't validate this handle Well technically wouldn't because we wouldn't drop it but to be on the safe side We don't want to swap I'll hold in this reference and we won't I'll show you in a second So this is also going to pass in the our handle And this is also going to pass in our handle So Let me go back to I guess lib So that means that absorb is also going to be given other This is going to be a reference to T. Oh reference to self actually Now this is going to look a little weird because it looks like we're never applying anything to our handle and that's true We don't but remember that so like Imagine this being map zero So this is also map zero the next time around we're going to call on this subset of the oplog This is going to be map one. So we're going to apply it to map one on this the next iteration around W handle and our handle will be swapped Do you want to always use the same hasher so the iteration order will be deterministic I don't think that's why I did it. I'm trying to actually there might be a PR. Let me check here What we got here. I'm pretty sure there was a PR that did this change Maybe I just let's do hasher and see what we get here commits It's a very good question. I don't actually know why I Don't know why that's there Actually, I suppose we can look at We can look at ev map right Apply So this is given the hasher Why is it given the hasher? Yeah, it just passes in the hasher everywhere. You know, I honestly have no idea I guess let's go back and blame Do a little tour of get here while we're at it so let's go ahead and look at source write Blame Let's go ahead here and look at ooh, that's not at all what I wanted to do Let's undo that Our hasher Wow, what an innocuous commit name stress test duplicates plus fix. I mean, I'm guessing some tests failed, but Don't know why Yeah, this is certainly where I introduced that change The drain must have been determined must have a deterministic iteration order Well, I guess here's the comment Let me Just go to that one Where's the Fine, I don't know this file Oh, that's real subtle All right, let me find that actual comment here Okay, so this is actually in the values So this is in the hash bag of values in ev map when it uses maps Because otherwise Okay, I'll I'll let you read this because it actually explains what the problem is basically If the iteration order is not consistent you might end up with the A Swap or move for example being applied to two different To two different sides of the map You might end up with swap removes being applied in different orders By the different sides of the map, which means that they end up Dropping and forgetting different elements from one another and you end up double dropping as opposed to forgetting and dropping I'll leave this up for a second so you can read it while I catch up a chat This is very specific to The value sets So it's a good question whether we actually Whether this This is something we have to preserve Now that said this change just always gave gave the hasher of the thing on the right so So in theory if we just give a reference to whatever is currently Well, it's not the thing on the right but the read handle So in theory if we just always give in a pointer to the read handle that should have the same effect Just trying to see here whether I see it makes sure to use the Same hasher in all of the like nested Hash bags and stuff. I think that's actually the primary That's really what makes a difference here. It's just making sure that everything is using the same Hasher That's what's going on So it's not that it's actually using the hasher each time. It's that if it happens to create a new Like bag hash bag for the values It makes sure to use the same hasher as it uses for the the map as a whole And as long as all of the hashers in the system are sort of the same This ends up not being a problem I see so in that case giving a reference to either left or right should be fine because they They always end up yielding the They all end up using the same hasher All right Sorry, that was me trying to work through what was going on While trying to explain it. So let me take one step back And try to actually explain what went wrong Okay, so Remember we need to make sure that every operation from the op log is applied deterministically This means that if you have If you have the values be for example a hash map or something or a hash bag in this case The iteration order of that bag also has to be deterministic That is it has to be the same whether you're doing it to the hash bag when it's in the left Or the hash bag when it's in the right for some given key If they use different hashers it won't be and so we need to make sure that anything in the val in anything in the Anything in the the type that we're creating the left right over has to be deterministic Which means that if it uses a hasher it has to use the same hasher But In order to do that we need to pass the hasher around so that if we ever say create a new Hash bag because someone inserts a key for the first time we need to create a hash bag We need to make sure that we create it with the same hasher as it's used everywhere else And so that's why this hasher is being passed everywhere now Because the hasher is the same in the left and the right map because they're clones of each other to begin with Because that's the case it's Well Yeah, because the Because the initial two maps are clones of each other their hashers are initially initial the same So it doesn't matter which one we give a reference to to get to the hasher because their hashers are the same So if you clone the hasher you'll get the same hasher regardless of whether you took it from left or right So ultimately all that matters here is that it it needs this this shared reference in order to ensure that Any determinist any non determinist any source of non determinism Like the hasher of the hash map is actually cloned throughout the system. So they also use the same source of non determinism i.e the same hasher and so giving a Reference to the the t on the other side as opposed to specifically the hasher should be sufficient Great. So our handle here should be enough There's a bunch of references to maps in the comments as something we can clean up later. I think We don't have meta information anymore What i'm imagining is that modifications to the meta can be part of the op-log And that way it's up to the implementation to make sure that it keeps that as an internal field somewhere Notice also that there's this scope here That's what causes the r-handle to go out of scope so that we make sure that at the time when we swap We no longer have that reference in there Do you even need the r-handle if you pass the w-handle anyway? Probably not The implementation could probably just use the hasher from the w-handle instead At the same time, I don't think there's really a downside to passing in the r-handle here Um, it keeps the trait simpler, but i'm just like I'm just paranoid that in the future someone will be like, oh, but I need access to the other one to something something So I feel like we given that we can trivially get at it. We might as well just provide it Is my thinking um Yeah, so the comment here right at this point We have exclusive access to w-handle and it's up to date with all rights The stale r-handle is accessed by readers through an r clone of the atomic pointer inside read handle An op log contains all the changes that are in w-handle, but not in r-handle. And so now we can do the swap So this is just actually doing the swap It after so it does the swap here, that's the atomic pointer swap Um Then as a fence to make sure for memory ordering reasons We want to make sure that we read the epic counters strictly after we did the swap Otherwise the cpu is allowed to reorder these instructions and that that's not okay So This is the step where we read the epics at the end of refresh right as I talked about in the beginning um Yeah, and as the note says at this point There's likely still readers using the w-handle because they haven't left yet Uh and second and first are optimizations. We no longer care about Um So flush is a an operation that's uh conditional refresh So it only does a refresh if there are things to refresh Uh set metadata goes away But add op is going to be a penned op And that's going to be an o Uh and we don't have this first optimization anymore So we're just going to do this That's all a penned op is going to be and all these methods that are specific to maps are now going to go away Um, we do have to figure out what to do about clear. I don't quite know what to do about clear yet Uh, which we I think we need for drop Uh fit fit all reserve empty random Uh apply first So Apply first and apply second um Are methods that are going to be on the t itself So if we go back up here This is not going to be self apply second. There's actually going to be w handle dot apply second And w handle dot apply first Which means that the actual methods down here Uh And I guess we can go back and add these to the documentation over there Um All of this like what you do for each operation That's all going to end up in um in the actual implementation of absorb for map That ev map is going to end up using Same with extend. Uh, there's no reason for us to implement extend here because extend is a map specific thing um We do probably want to implement d ref For right handle So that you can easily access whatever the inner type is through the right handle Here we only need to implement. This is where it's like It's kind of silly for us to have this requirement. It just for d ref given that it's not a requirement It's not actually a requirement, but we need it because it's on the struct and it's on the struct because we need it for drop um There we go And these tests, uh, I guess we'll port over later It seems like the requirements for implementing absorb are going to be very subtle um Yeah, I think absorb is a very subtle trait and it could really be that um It could really be that this needs to be an unsafe trait. I think you're right Um, which is like this is already in an unsafe block. I'm pretty sure um Yeah Uh, could you elaborate a bit more on what the problem with clear is? Yeah, so Uh, let me go back here up to drop Okay, so in drop or first going to do is synchronize the two maps so that they have the exact same contents um, and then what we want to do is we want to drop both maps um But we don't really have a good way of dropping both maps because In the first map we want to forget all the values in the second map We want to drop all the values, but we don't have a way to say forget all the values Right if we drop it they get freed Um, we could just forget the whole t but that doesn't seem right We actually do need to drop the t because there might be allocations in there like the allocation of the buckets for the hash map For example, so we can't just forget the whole t We need to sort of clear the t we need to make the that make one of the two t's be Emptied by forgetting but still dropping things that need to be dropped And the other t is the one we're actually okay with just dropping But that means that there has to be an operation that's like clear this while taking into account the fact that you're um While taking into account the fact that you are Uh, actually not allowed to drop um So I think maybe what we want here is for this to be a method on absorb So this has to be like, uh Maybe it should actually just be forget So this is, uh um Maybe drop forget or drop first actually So this is, uh Drop self But do not Deallocate any shared values Do not I guess drop any shared values So this will be dropped first Did that make sense? Um, all right. I'm sure there are still errors in this file, uh, but I don't immediately see them So let's switch over now to read handle um So for read we have a bunch of uh, actually let me show you here Oh source We can remove shallow copy. We're not going to need that here um and read Let's also go ahead and move read mod rs to just be Source read dot rs because that's nicer to work with Oops, that's not at all what I wanted to do Uh EV map source read mod to source read rs Um, so there's read rs And then there's also a sort of sub directory for read, uh, and that includes some some handy helper methods. So Uh, a read handle you can't just deref into the the t because it needs to also make sure to update the, um Update the epic counters appropriately. So this is why I have a read guard type. This is similar to mutex So the idea is that when you have a read handle, you can sort of take the guard You think of it as calling lock except you're not actually locking anything Um, that gives you back a guard the guard d references into the t and then when you drop the guard it increments the epic again um, so that's guard and uh factory is um You might want to be able to To Clone new read handles without actually storing a read handle Um, this might be because the read handle for example is not sync. Um, we'll look at that in a second. Why that is um, but imagine that you You want to store like a lacy static or like a just like a completely shared arc That people can grab new read handles from you don't want that thing to be a read handle because it would it would have It would require that the read handle type itself was like send and sync and all that stuff, which it is not So that's why we have the read the read handle factory type, which is a sort of A stripped down version of read handle that only allows you to create read handles. It doesn't actually have its own epic And read ref is Uh Kind of uninteresting now that we're not dealing with maps. Um, so I can get rid of that Uh, actually let's look at read ref first Is that there I wonder Yeah, so this just implements like map methods around guard I don't know why these are not implemented directly on guard. I forget I think it's because of the iterator trait actually Um, so but we don't need those anymore here. That's a type that would be provided by the implementer of absorb So we can get rid of read ref Um, we do need to look at read dot rs. Let's start with that one There's no longer an inner and there's no longer a values Uh, we no longer have random state or hashers Um There is no longer a read ref Uh This documentation goes away Read handle now just also takes a key and an o But it does not actually no need to know about absorb at all. In fact, it doesn't even need to know about o I don't think Yeah, I don't think read has to know about o That's kind of neat ah Read handle right because the read handle never sees the op log nice Um, it just has an atomic pointer to a t Right just like the right side has a A handle to a t and the read side has a Sort of atomic pointer to a t Um, it has epics. It has a Um, it has sort of a a shared epic counter for itself So it's shared between the reason it's an arc, right? This is shared between itself and the right handle um Epic eyes like a local cache of the epic, I think And my epic I forget why exists, but I guess we'll find out later Right. Okay. So this part is a little weird. Um So read handle takes a, um Read handle takes an immutable reference to self Uh in order to do In order like it has to implement deref, right and deref Well, actually it doesn't have to implement deref, but the Call to get a guard Take currently just takes an immutable reference to self to the read handle But under the hood, it's not really an immutable reference because it needs to update the epic guard So it should sort of be a mutable reference to self. It just feels weird for A Read hand like a read operation to require mute self But I think this is actually something we want to change that's sort of a an oddity of how it currently works So I think we're going to tidy this up a little bit This is so okay, this is kind of funky the reason that it I've implemented not sync this way is because um At least it used to be that and this might still be the case that there's a You need to like use a nightly feature in order to specifically implement not sync. So instead, I just inserted a type that is not sync What this might actually Making this change might mean that we don't know that we no longer need the read factory So we're going to switch this around a little Um, this is in fact a u-size This is the index into epics that belongs to us so the index in epics that Uh is equal to this And then my epic is sort of a cache of what our epic was that was an atomic because we wanted to be able to take An immutable reference to self if we're going to change that now that can change which is nice This now just takes T Doesn't have to take any bounds So the reason we need to implement drop for read handle is because We have to make sure that Uh parody must be restored is not quite right here But we want to remove our own We want to remove our own epic counter from the sort of master epic counter list um and we need to do that so that Uh, so that we don't have the writer just Continuously look at all these epics that are just no longer relevant um Oh, I think what I mean here is that uh epic Must already be even Uh for us to have at mute self So okay to locks since we're not helping holding up the epic anyway Yeah, so the drop here is just removing ourselves from the global list Uh, we can implement debug It's so nice to get rid of all these bounds uh, although we Do and we don't have any bounds actually for debug for read handle because it's really just printing out integers Um, because remember the read handle Until you sort of lock it like unless you until you increment your epic You can't actually read through the read handle. You must implement the epic first So if you wanted to just debug a read handle, all you can do is print out information about the epics Uh cloning a read handle is Really just creating a new read handle So this should be fine And the fact that you can clone a read read handle now and the read handle is synced means we no longer need the factory Uh, so let me go ahead and remove the factory entirely Our source read factory Because now you can just stick the read handle in a in an arc if you wanted to clone it It does mean that there's technically an additional epic for the writer to to use But it's unlikely that you had many factories anyway Uh, all right, uh, creating a new read handle Really just requires that you give me a t and an epics And it again just boxes the the t stores the pointer in an atomic pointer that is a That is reference counted so that it can be shared among all the readers And the the the distinction between the outer function and the inner function here is not clear. It's a good one Really I feel like it would be nicer to just have this be pub crate new Probably Right, isn't that just better? That's definitely better Let's do that instead much nicer. We don't need this extra freestanding new function. That seems silly um Great, we no longer have this thing And my epic is going to start out at zero I forget why it caches my epic. I guess we'll find out soon um Factory goes away I love just getting rid of all this stuff that used to be there Uh, we no longer have any requirements on t Okay, so handle here is the one that used to be um Used to take just a reference to self that's now going to take a mute self um Hasher is going to go away These are going to go away These are just like uh convenience methods for take the lock and then call this method on map Those can all go away now Makes me very happy Very very happy very very very happy Is destroyed So destroy is um if the right handle goes away then Then it takes both maps with it. So reads need to start returning like none. Uh, and that's what is destroyed does Uh, it contains key is a map method contains values a map method Map into is a map method Nice and all the tests can go away for now um We don't really need read to be different from handle. I don't know what the best name for this method is um of the t T kind of being refreshed Or t has been destroyed this function returns none Um, maybe read is the right method, right? This is sort of like lock except it's not really lock So this is going to be a read guard of The lifetime here is the type lifetime of self um So here's what we're going to do. We're actually just going to steal this whole implementation We don't need this to be multiple methods anymore um So we're going to go here and steal that over here Uh Does that new change you made just break the clone in bowl Oh, it probably did Oh, that's why that's real dumb I guess that's why it was there. Um Uh new with arc Which takes a uh arc Atomic pointer of t Fine fine fine. It's I still think it's better for it to be on here So we're gonna it is a change, but Uh, so that's going to call self New with arc Interax And this is not going to be public And then clone has to call new with arc great I think read is a decent name for this It is a little weird to have a read handle that you call read on in order to get a read guard that you can read from Like there's a lot of read But I really don't want to call it lock because it makes it sound like you're locking something when you're not um It could be guard But like read is pretty appropriate like I want to do a read it could be start read Or like use or It could be enter enter is not bad Enter is not bad Borrow also not bad, but not technically accurate Um Yeah, I like enter enter is good Uh All right There's a bunch of comments here. That's fine Um I honestly don't know Why My epic is stored separately because we could just update Self dot epic directly But uh That epic is self dot Yeah, I honestly don't remember but I'm not going to change it now Uh epic plus equals one Yeah, so this is where you see we first update the epic counter um And then we do a fence to make sure it doesn't get reordered to after the swap Or after the the read of the pointer, then we read the atomic value And at that point because you can think of like increment of the counter sort of locked the data structure We know it won't change under us now And so now it's safe for us to to do this This might be none if the pointer is If the pointer is null Because the writer has destroyed it Oh has not yet been initialized. Why would that be the case? This sounds like a case I didn't deal with Um I don't think that can happen This map has not yet been initialized I think this has to do with the the first optimization but Like yeah, I think the idea here was that until you call refresh the first time Uh, you can't read anything from the map I think is the intention But I don't think that's the distinction we want to make here, but it can be null um It can be null if we read after the right handle removed it But at that point it doesn't matter whether we restore parity Hmm So this is going to be old epic Previous epic to be very explicit about it The map has been destroyed So restore parity and so this is going to be No, I think this really is let Fetch add returns the value before the add Right so something's wonky here Let me check this in read mod just What this used to do Epic was set to the old epic So old epic Plus one does not Oh, and then it sets the bit I see I don't know why I did this why I ended up using the high bit instead of even odd It's a good question Also, probably something Someone can submit a pr to fix I I want to avoid changing as much of the concurrency primitive as I can Both just to show that it's possible, but also because it means that if things go wrong They're probably because of the change we made and not changes to the concurrency algorithm So I guess this is previous No, I'm in the wrong file um This is going to be previous epic plus one But if I'm using previous epic plus one everywhere, then why don't I just store the epic? That Epic is self dot my epic Great Uh, and then this is going to store the epic. Oh epic doesn't look like a word anymore Uh Oh, this uses the old epic Oh my why Okay, so this means that the guard Now Uh Is going to be off by one everywhere Yeah, so this needs to not be a plus one anymore And I think that's the only place it was used Okay, great um And this is also just going to be epic And set the high bit Um, I don't think I want this if no refresh has happened to return none Guys, I think that should be up to the t whether or not The value is valid before the first refresh If the t has been destroyed this function returns none um Yep To me it sounds like you're saying epic. Yeah, I mean it's Epoch Epoch is a different way to say it, but I think epic is also a valid pronunciation Uh everywhere in read handle and read guard you need to swap u size. Yeah, that's right. Uh, this I already did it I think Um epic I should remain u size because it's an index into the epic slab. Uh, so it should be a u size um, but if there is another Uh, this needs to be u 64 And this needs to be u 64 and I guess in guard Uh, this needs to be u 64 This needs to be u 64 u 64 u 64 Um Right Uh, there's no longer values in here T being read cannot change and runner attempts to call right and refresh that call will block until this guard is dropped Um Yeah, that's fine That's fine That's fine Yeah, so you'll notice that guard has some like handy mapping methods. This is the same thing that you see on something like If you use ref cell Or in fact, I think mutex guards as well You can map like if you have a mutex guard to a t You can call map on it to map to an value that's inside of the t So you get a map guard like you where you was like a field of t or something But you were you retain the guard and this this is sometimes handy if you want to pass a reference To something that's inside of a guard you're holding to some sub function without giving it a handle to the whole outer thing um And you see that for read guard we can actually implement as ref t d ref to t Uh, the drop you look at already looked at Into iterator. We will not implement for read guard All right Nice There's a bunch of types here now that we don't use like all of these guys. That's nice Let's see if this compiles probably doesn't Um This has been an epic journey. That's funny Uh, is there a way to pass in the field? Into size of instead of the type name so that changes the type And the start automatically changes what size of a value it's to yeah, there is um So you can do um, I think it's just Size of val Let me look this up I think it's size of val Uh where you give a reference to t. Yeah, there's also size of val Uh size of val raw I don't know what size of val raw does. Oh, it's just a raw pointer. Yes, we can do size of val of Like self dot my epic Um Although we would also need to deal with this guy, which is a little more annoying So I think I'm going to keep it this way for now It's a little annoying that I had specifically Actually, this might just not compile So if I do if I do this and just do If I did one like u32, what would this say? I'm just going to leave this for myself to see whether this stops compiling Because if this can just be an untyped one and it gets coerced into the right type that would certainly be nicer um All right, we've got some remaining things that are weird here Uh, so read handle factory went away. That's nice Uh read handle does not have an o. That's also nice This should be read read handle new Which I guess means that this should be right handle New and since we're exporting the type it can just be this Uh, which means that here we probably have a freestanding new function Which should really just be a function on right handle Uh that returns a self Like so All right, what else is it complaining about? Uh on right line 28 Um, oh All right I have to pass it everywhere now. It's very sad. I wish it wasn't needed for drop But it kind of is um Okay, so it does actually complain about u64 ord with u32 So that suggests that if I just make this one Okay, yeah, then I totally agree with you. This should be Um So this would be in regard That should be a one and size of val Of we just have to be careful here that we're not giving a reference to a reference because then we'd end up with the size of u size Um, so this should be self dot Let me just make sure this is in fact true. Yeah size of val self And same and right This then should be So here it's a little trickier because We don't have something with that type um But we might be able to get something Uh, we can't use any we can use last epics actually Uh, so what we can do is say um epic type is self last epics get zero unwrap or zero Uh, we need to unwrap or because last epics might be empty and then we can do size of val of epic type and we can do um actually This is real really annoying, but we're gonna do this map To one uh Yeah map to one unwrap or one And then this is going to be epic type So I guess epic unit Is a better name This is a little bit of an ugly hack But the idea is that we want something that has the same type as the epics And the way we do that is we just get something that is an epic Um, but we need it to be a one so that we can have it be unit and shift it appropriately So we map whatever value you get out to one and we also need to unwrap or in case there are no epics in last epics But this will ultimately get us a one in the correct type As long as last epics is of ecovue 64 which it is A little bit of a hack, but it works That map doesn't care about its input. You're right Uh You're right. That won't work because the map here is going to generate an i32 good catch So We're gonna Well, we could do operations on x but actually Here's what we're going to do instead We're going to do unwrap or Then we're going to do epic unit is one Uh What copy and define because it get returns an option. That's a reference. It's an option reference into epics This is going to complain with Value red but not used and then we can just allow Whatever that lint is So that lint is going to be a I guess the lint is going to come eventually That way we avoid doing like ugly unnecessary arithmetic So for example, if we did e minus e like map e e minus e plus one We could do that but it that seems even hackier Xor with zero would also work. I guess but I think clippy might complain about that too because it's like a useless operation The compiler might not complain but clippy will probably complain Whereas if here we're being very explicit that we're doing this just to get the type All right, what else we got 138? This needs to be enter line 60 Uh This is an atomic u 64 I love the compiler Thanks for telling me all the way some in which i'm wrong compiler. It's very helpful Right 231 This is going to be a manually drop new This is going to be All right, this is going to be As starting with t or so what we're using here, right is uh, we have The value in the right handle is in manually drop because it wouldn't be safe for us to just drop it um We could alternatively have the value in the Read side be manually drop That might be better. They're sort of equivalent Like if you're mostly doing inserts, there'll be more things in the right handle if you're most if you're doing a bunch of deletes So it'll be more things in the read handle. I think they're they're basically equivalent. Um, so manually drop Where this gets tricky right is here. We have the w handle and we want to get at the inner type We we the because the r handle is a box t we're an atomic pointer t And the w handle is a pointer to a manually drop t. That's why we have to do the cast Um, I think there's a nicer way to do this, but I forget Yeah, we could deref But this seems fine to do as a cast anyway Now that's fine. This can just be r handle as Mute manually drop t. All right, what else we got? um Actually, this is another good point. Um This logic of we're only going to drop things once would be sort of broken if someone passed as a t that had items in it already Because those items would presumably not be aliased I guess it would be Yeah, so so um to explain that in a little bit more of a sane way So we have our constructor for the left right here, right? It takes a single t it clones the t and makes one thing the right half and one thing the read half But by cloning it we mean that means that both the two halves Own their values if imagine that they're non-empty like the the caller provided some values in the t initially So if they did that Then those values would be owned on both sides and actually need to be dropped on both sides to free the memory But from from the point they give it to left right and forward um We're only ever going to drop one of them because we're going to assume that they're aliased But I think this is something we're going to leave to the caller to figure out. There's no one's safety here It's just it might end up with a memory leak if they If the t is non-empty Uh, no if t is non-empty, um Values may Not be dropped unless you handle them specifically in Apply first unless you Drop the in Unless you Specifically drop the initial values in apply first This just like smells a lot like a footgun. So there's an argument here for we should only provide a new that Does debug does that uses default. So it also always starts out empty You could require the for this new t approach you have t implement shallow clone Um, so t itself should not be a shallow clone or a shallow copy Because then you would end up with both both halves pointing to the same map, which is not what we want. That's what shallow copy would do um So I think One argument here is we make new unsafe. It's not really unsafe. Like it's not memory unsafe So it's not it feels like not really the right thing to do. Um But this also seems like a big foot gun Um Yeah, I think I think we really just want this to require a default Um, and then I think we want to create a t default here And a t default here It's just going to lead to much less pain. Oh, we could have a new trait. That's like, um An initial shallow clone, but I think we should just go this way instead. Um, all right, what do we have left? No method inner Um No method inner Oh, this should just be inner It's not a method. Um Ah So here Manually drop I think does not implement dref mute. It only implements dref I'm not entirely sure why that is actually Um, no, it does implement dref mute So, uh, but it doesn't forward the absorb trait Um, so here The one there are two things we could do. So, uh, let me explain why this happens So if you have, um, some type that implements absorb And then you wrap that type in say a box or a manually drop or some type that implements dref mute Um, that trait will not be implemented for the wrapper type The reason you can use a trait like, um, clone or equality or something through box is because, um It's because box specifically forwards traits that it knows about so if we read down into box um, you'll see down here that It implements clone for box t where t is clone It implements default for box t where t is default it implements Um Eek for box t where t is eek. So it forwards all these traits, but box doesn't know about absorb So box won't forward the absorb trait sort of through it. Um, and same thing was something like manually drop now There are two ways we can fix this. Uh, one is that we can have blanket implementations here. So we could do impulse. Oh, um, sample To absorb Oh for t for, um, mute t Where t implements absorb Um, and then just have all the methods forward Uh, and then do this we could even do the same for like Have a blanket implementation for box t a blanket implementation for um manually drop t This gets pretty tedious But it will make absorb the absorb type a little nicer to work with um, like imagine that someone had implements um Absorb for hash map But then they pass in like a manually drop hash map Well, they pass in a mutable reference to to a hash map that wouldn't work in our current scenario because those types that those pointer types Do not implement the trait Um, but it does lead to a lot of duplication, which is pretty annoying um In fact But it does mean that we have to enumerate all of these. Um, the alternative is that here we can just like D ref it through them the manually drop like this Um, and then apply second will be the specifically the t Uh, am I being stupid? Probably am Don't we require that t is absorbed? No method apply second found from oh, we need to use the trait. We already use the trait All right, so the other way we can go is absorb Explicitly say that that's the thing we want to do uh, w handle Uh No function apply second is it not call? Oh, it's called absorb fine absorb second Actually, maybe this is just maybe the d ref will actually handle this Uh, w handle. Yeah, it probably will Um It'll d ref first and then realize that the d ref implements the trait Um, the problem still holds here though where actually here they're gonna specifically Yeah, so imagine that someone writes like Uh, like Left right new I guess this would be like left right new And they try to give the types a um box hash map kv And some operational type um now If they did that then Abs and let's say absorb was implemented for this type Absorb would not be implemented for the type they're trying to pass in because box is not forward absorb and they can't implement um absorbed for box because this is the the sort of coherence rules for traits that a Third-party crate you can only implement traits for types if you own either the trait or the type in this case They don't own the absorbed trait we do and they don't own the box type the standard library does Um, so we actually do need to provide these implementations There's a separate discussion here for why is oh Uh, uh an associated type why is oh not an associated type? Why is it a generic parameter of the trait? Uh, we can go into that in a second I do want to simplify this a little bit though by uh writing a little macro Uh, and the macro is going to be forward um And it's really just going to take a t which is a type I can never remember what the actual thing is here. Um, and it's going to do this uh for This may just have to be a path Um because a t would include the generic parameters Um, and then we're just going to have that implement all the methods um And in all cases, it's really just going to rely on the fact that the type d refs um To call self absorb first operation another absorb second and self dot drop first Um, we might be able to yeah, so someone pointed out you can maybe implement like absorb for you where you implement d ref mute um I don't think we can do that because uh that if you Because the proposal is implement like to you Absorb Oh for some you where you implement d ref mute um t And t implements absorb Oh And just have the forwarding be here. Um, but if we did that, this is a blanket implementation for you So this means that no one else could implement the absorb trait for their own types Um, because the compiler would say well this overlaps with this implementation Even even though we have a wear bound here um This would be fixed. I think with specialization But specialization hasn't landed yet. So let's not rely on it Um, did I write a borb somewhere a borb? Because if so, that's hilarious Um, I think it's just tie. Yeah, it might just be tie Oh, but also it has to be colon path Uh, what is it? What is it complaining about? Oh unused macro. That's fine. So we're going to forward through, uh, box And some other types later Actually, here's what we're going to have to do Well, I'll explain that in a second. Um, yeah, so actually I may have to do it now Uh, I think what this is going to end up looking like is this Um, and this is going to be a tie This is going to be a macro book Little book of rust macros I love this thing. Um macro rules. All right, show me what the different captors are Uh, I think it's ident or tt are probably gonna work both gonna work So this is gonna be t Oh, there's a borb. I found it Uh, and this is gonna be Oh, this is gonna be through where T um So many of borbs um And this should be absorb of oh I guess unsafe impulse. That's fine Uh, and this obviously is gonna have to Uh, do a dref mute The whole point of these types is that they dref into The necessary type Um Wait, why can't this um first Why can't this do without recursing? That seems like a lie Right, okay, fine. Yeah, because it's taking a mutable reference to self fine, fine. Okay. And this is going to be absorb second And this is going to be absorbed drop first All right So this now can forward t Uh, the the reason that the t needs to be explicitly passed in here is because of uh macro hygiene So you can't talk about a type, you can't talk about any identifier inside of a macro that exists outside of the macro and vice versa. So if we said like, if we had this be just a capital T, then the thing that gets passed in the second argument couldn't also use T, those would just be completely separate T's. So we needed to pass in the T's so that we can name the T in the type. And now we can do like mute T, we can forward to like arc T, we can forward through manually drop T. What else might we want? I think realistically, like, what would be people actually want to pass in here is the real question. Like mute T isn't going to implement default. Arc T, I have a hard time thinking that anyone's going to stick an arc T in here. To be honest, I have a hard time people are going to stick box T in here too. It was not impossible, but an arc T doesn't make sense because they're not going to be able to share this type of cardless. Manually drop, maybe they would pass in, but it seems weird for both of these to be manually dropped. So for now, I think we're just going to forward box. Aborb is literally everywhere. Chad really got obsessed with Aborb. I'm trying to trying to save you from all the Aborbs, you know, they're out to get you. We could explicitly call DRF mute. That's the other things that we could, but we would have to use the trait. You have to write like use ops DRF mute or borrow. I forget where there's ops and then call self.drfmute. But this is the more concise way to do it. We could use, oh, that's a good point. We could use T absorb first self, maybe that's a five. Yeah, that's nicer. So this way the DRF, the as ref actually is probably going to be used here to course this into a mutable reference to the inner type. This won't work with DRF mute. This will only work with as mute, I think. Whereas this one will actually work with DRF mute. I don't know if this is nicer, but I think it's probably fine. Given that we control the input anyway, this is a little nicer. Oh, you're right. ARK doesn't implement DRF mute anyway. ARK and RC, no, ARK and RC do not DRF mute, so they don't make sense here. Abort the aborb. I think box is really the only one I can think of where someone might want the left and right types to be box types. I don't think it makes sense for them to be ARK types because they shouldn't be shared. I don't think it makes sense for them to be manually drop. Yeah, no, I think box is the only one I can really think of. Box is sort of special. Yeah, my plan is to switch EVMAP to use left, right in the stream as well. Do you expect there to be crates that expose specific common collections through left, right? Maybe all of them exist in EVMAP. I don't know yet. But I certainly know there's a lot of appetite for a single value version of EVMAP, so one that doesn't have a value set. And this change will enable that. All right, what else we got? Read 138. Right, so okay, this is where it gets a little weird, right? So this is on read handle. So remember how read handle enter takes immutable reference to self. Well, read handle used to have this method that's like is destroyed. And is destroyed used to take an immutable reference to self, which makes a lot of sense. Like you're not, you're just checking whether it's destroyed. In reality, though, it calls enter, which in turn needs to do all the weird work to like take the epic counter and stuff. But I think we can actually optimize is destroyed. Because I think all it needs to do is check whether the R handle is null. So I think we can actually just do this. Nice. Much, much nicer. Because because we're not dereferencing the value, we don't need to increment the epic, right? To tell the writer that we're like consuming the point or anything. All we're doing is loading it and seeing if it's null. If so, it's been destroyed. That's nice. I like that. So that one can stay immutable reference to self. What else we got here? Why is map ref and map opt? Why are they pub super and not just pub? I think they should just be pub. I'm not sure why this has a map option that seems excessive. Like, why is first of all, this should just be called map. And this should because so in general, this is another observation. So read guard D refs into the inner type. So imagine that the type T also had a map function. That's really awkward because now when you call dot map, you're going to get the read guard dot map, not the T dot map. And this is the same problem that arc has, for example, with clone, where if the inner type implements clone, and you call clone on the arc, it's not immediately obvious whether you get a clone of the arc or the clone of the inner T. And this is why for our clone, for example, the recommendation is that you never write like my arc dot clone, you always write arc clone my arc to indicate specifically that you're looking to clone the arc and not the inner type. And I think we want the same thing for read guard. This is the same thing if you look at ref mute has a map method and that similarly is not a method, but it's a function that takes like this, which is a self. So that way this is not an inherent method on self. It's not something you can use dot for you would have to write read guard colon colon map. I don't know why we have map opt here is map opt really that different. Let's look at what ref mute, which is the thing you get back from ref cell does. So ref mute has a map. It has a map split. And it has a leak. Why is map split? Okay. Oh, I see. I don't care about map split yet. Map is good enough. Yeah, notice that map here does not take a self, right? It takes an or a guy, I guess we can use the same naming here so that it's not an inherent method. And I think we're just going to get rid of map opt. Yeah, what did I do? Great. Does is destroyed makes sense if it returns false? It could also be always been destroyed right after. Yeah, I mean, is droid destroyed is sort of a, you can think of it more as like an optimistic operation. Like, tell me if it currently is this is the same thing as on arc. There's a like, give me the current number of reference, like give me the current reference count, which is also a useless method because the reference count is like it might be changing, someone might be cloning it right now. But sometimes it's useful to have access to the information, even if it's sort of approximate. It might be that there are other invariance in your program that make it so that approximate information you know is precise. An example of this is, if you are told that the reference count is currently one, then you know that it won't change because the only reference is the one that you have that you called reference count on to get one. I don't know whether the same applies to is destroyed, but it's a cheap operation for us to provide and the caller can't get it any other. Well, they could call enter, but there's a much cheaper way to giving it. Do you have a benchmark for UV map? Yeah, I mean, there's a benchmark for UV map in in the repository. We can run it. I haven't, I don't know if I've run it on this computer before. So I don't know if I really have representative results. This abstraction layer shouldn't matter because of monomorphization. The code should end up being the same. The only place where it will be different is rust generally doesn't inline across crate boundaries, unless you explicitly mark a method as being inlinable. So that might be a small difference, but my guess is it doesn't really matter. This map means you can't use method syntax, maybe just use a different name. So this is the reason why the standard library does it this way and doesn't do self is because no matter what name you choose here, it might be that the T has the same type. And so it's always better to provide this as a class method or a type method as opposed to a as opposed to an inherent method. It's true. You can't use method syntax, but that's sort of the point. Now what do we have? Right. So this is the the error we expected to get, right? Which is we had a warning here saying this is an unused assignment. And we want to allow that specifically on assignments. We want a one in the type of the epoch epochs without naming that type here. Rust does inline across inline across crate boundaries with LTO. But LTO is a little bit more of a pain to compile, like it's generally slower. You can use thin LTO, which gets most of the benefit of LTO without completely killing your build times. And I think that does inlining across crate boundaries, but it's off by default. Nice. So I think that now compiles. That's cool. So we now have this left, right type. And now we need to figure out is whether we can implement EV map in terms of this type. There's obviously a bunch of documentation that's missing here that we need to fill out. But we now at least seem to have the concurrency primitive isolated. And then what's going to sort of be the big test is whether EV map can be implemented in terms of this. Maybe run into some weird problems with generics. I don't think we will, but we're going to find out. All right. I'm going to take a quick bio break and I'll be back. Just chat amongst yourselves. I also just realized I have a pause screen, I think, but I forget what it is. Let me try this. I think now you should see like a pause screen. Is that right? Maybe with, it's a pause screen with J on it. Or maybe I've set up my key bindings run. Oh, nice. Okay. Sweet. And then this is me. And this is my screen, I think. So we should be back to screen now. Yeah, I love the llama. Jerry's great. All right. Where were we right back to EV map cargo? All right. So the trick now is we're going to have EV map work in terms of left, right. That's the plan. So that means it's going to have a dependency on left, right. And I guess it's going to be version one. I guess we don't really, I don't really know what the version of left, right should be yet. Should be like version 0.9 is pretty close to stable. It's, it's a transliteration of something that's worked for a long time. I need to, what for workspaces? What is the recommended way? Specifying dependencies. No. Workspace. It's been in multiple packages in the same work. It used path dependency points to the local packages and used local version to development. You can create creates IO Yeah. Okay. So I guess that's what they want you to do. So it'll be version equals this path equals dot, dot left, right. So now we're going to start with sort of part two, which is going to be EV map. Yeah. So having this, I'd grab this directly from here, which is if you're working in a workspace, you can use a path dependency to point to the local packages in the workspace during development and then use create, sorry, our version once published. Now, whether you then have to like rewrite this each time you publish, I forget. I don't think you do. I think this will just work. But it might get annoying during publishing. Once you do like version changes, this gets hugely painful. But I think this will work fine. All right. So what are we going to have to change here? Well, ethics is going to go away. Predator is going to stay. Operation is going to stay. Right handle and the various read things are going to stay. But they're mostly now going to be wrappers around the inner type. Read handle factory is going to go away. Shallow copy is going to stay options going to stay. Yeah. So this is an example where EV map actually does need a new it can't use a default. Because it wants to be able to specify like the hasher for it wants to be able to specify the hasher for a hash map, for example. So I do think we actually need the do you think we need the version of new that takes a thing that's clone. So this is going to be I think what we're going to do is call it new from empty. Note the T must be empty. As otherwise apply first would only forget values from would only forget would forget not drop initial values from one of the two halves. So this is going to now take a T and it's going to do is going to take T where T is clone. It's going to take a T here and T here even for default. I'm not sure if we want to do this because remember for hash map we need to ensure that they're deterministic. So they need to have the the two maps need to have the same hashers. So I think we actually do want this to be clone. So we're going to have like let T is T default or actually just going to call new from empty T default. Yes, as I mentioned before, we can't use shallow copy for new because we don't want to shallow copy the entire T. We want the T's values to be shallow copy. If we shallow copy the T, you would have two stack like hash maps that point to the same underlying buckets, which is not what we want. Okay, so we now have new from empty. So this is now really going to do right. So this is where EVMap does something kind of weird. It marks the right handle as ready, but it does not mark the read handle as ready. I think the way this is going to work is the the initial inner is not going to be ready. And then we're going to call like a ready method on. We're going to append an operation to the right handle immediately that sets it to be ready. So the for the operations up here, Mark ready. So here what we'll do then is let Rw is left, right, new from empty T, or I guess inner. And the W is going to be mutable. And then we're going to append immediately append off operation, operation, Mark ready, and return the Rw. And then actually, this is not going to be, this is really going to be our read handle, new and our right handle. So EVMap is going to provide its own wrapper types, its own wrapper types around the left and right read handles that do things like provide specifically provide map specific methods on them, like handle, hander, what am I doing, right handle. So right handle, for example, is going to have an insert method, which left rights right handle does not. And we're going to have to actually write these implementations, append off does not exist. Why not? Right, because inner doesn't implement absorb, which we're going to have to get to. This is probably all fine, because it all goes through construct. So we're actually going to remove EV, I guess we have to go to EVMap now, we're going to remove source read factory, that one's going to go away. And inner, so inner in EVMap is kind of interesting, inner in EVMap is now going to be the actual type that gets shared. So we could still do what it used to do, which is this like, as a feature to choose the map implementation. But nowadays, now that we have this like, fancy generic trait, we could implement it separately for like index map and hash map, for example, and have those just be two different types. I don't really want to do that because it would mean that I have to duplicate the full implementation of inner and read handle and write handle for each type. This is where it would be nice to have a collection trait or like a map trait, because that way, EVMap could be generic over its map implementation, while using the underlying, the underlying left, right, but barring that we're just going to say that you can choose it with a feature flag. All right. So it's really just going to hold a map implementation of meta and a ready. So inner is actually fairly similar to what it was before. And let's see here, all the debug stuff is the same, all the clone stuff is the same. All the construction stuff is the same. Mark ready and is ready is going to be, there's going to be operations. So these are just going to be, I don't know why these are even here, these should be fairly straightforward to deal with. So I just want to get rid of them and do the same thing I do here, which is mark that as pubcrate. Okay, so inner is easy enough, inner doesn't really have to change, because it's just the type we're going to share. Right is going to be simplified significantly. So right is now just going to have a handle, which is going to be a left, right, right handle of, and this is where it gets tricky, of a inner K manually drop VMS and an operation KV. And I wonder whether we can, we can try to implement the first and second, first and second optimizations here that may end up working. I'm not quite sure. Oh, right, we need to implement absorb for inner. So I think the apply first and apply second things are currently in the right file. Yeah, they are. So these are going to be methods on inner instead. And I think the way I want to express that is to probably leave them in the right file, it seems more appropriate that it's here than an inner. But I might come to regret it. Left, right, absorb operation KV for inner KVMS, not a lot of generic parameters. Absorb first. Absorb second. And we're also going to have to do here a, what did we call the last method, a drop first. And that one is unimplemented for now. Oh, yeah, I mean, now that now that this is generic, now that left, right is generic, you could pretty easily have any, any type you want as the inner type, whether that's a binary tree or a heap or a map or a counter as we talked about before. Arguably, that's the other thing we should do is try to implement like a test suite for a simple counter. This one is going to be a little bit annoying. And I'll talk about that in a second. Actually, I can maybe talk about that now. The trade absorb operation type, type of requires, right, this is unsafe. And KVMS. Oh, man. And that's going to require, I guess, actually, that's going to require all the different types we have here on right. It's going to be pretty annoying. Right. And this is now missing operation mark ready, which really is just going to be inner dot ready equals true. And same thing down here. That's a new operation we added. That's fine. Okay, so we have absorbed for inner now, which should mean that now we should be able to write to construct one. Right. So here's where this gets a little annoying. You see how it says the trade bound inner, basically, this is that absorb is not implemented for inner manually drop. That's true. We implemented absorb only for inner of KVMS, not K manually drop VMS. And the manually drop bit here is a little bit of, I think I mentioned it briefly earlier, a little bit of trickiness that right handle does, which is instead of trying to keep track of instead of manually calling like forget versus drop in apply first and apply second, what it does is just if we're applying first, then it uses a type that has manually drop V as the as the type that absorb gets called on. And if it's apply second, then it uses just V as the type that gets called on that won't really work anymore. Like this now just has to be V always. But I think the way we're going to get around this, the way we're going to be able to keep that simplicity, which was kind of nice, is that in apply first, or actually, no, the way we're going to get around this is to have, yeah, no, it is going to be V absorb for V is that this is going to take mute self. And it's going to take other. And what we're going to do is we're going to say that inner is going to cast self to be a to be a manually drop. It's not quite as nice. But I know that it's, I know that it's okay. Yeah, so this is going to have to be unsafe. Perhaps unsurprisingly, which is we're gonna have to do a mem transmute here. Okay, so let me write this out and then explain what it does. Let's see if that works. Yeah, so we're given in here an inner KVMS, but we're told to absorb first. What that means is we shouldn't drop any values, we should only ever forget them. And instead of trying to make sure that all the code below here, like that we explicitly call forget anytime something might drop a value, like, we wouldn't be able to call just clear on a vector because that's going to drop them, we would have to like, walk the vector and forget each element. Instead of doing that, what we're going to do is just change the type of inner to have the values be wrapped in manually drop so that dropping them will not actually drop the inner value. So will not actually drop the inner value. This is a super sneaky little trick. And what I have to do here is cast the self that I'm given, which thinks that it owns the V into the same value but where the V's are manually dropped. And I can't do that with a simple cast like with an S because the compiler doesn't know that these are the same type at all. Clearly what I'm doing here is super unsafe. And so I have to use mem transmute to transmute self and the syntax here is transmute is a function that takes two generic parameters, the from type and the to type, the from type is dictated by the argument I pass in, and the to type is what I want out. And you can you can see that if we go to transmute. Uh, where's the yeah transmute to you takes T gives you a U are all types of manually drop in the generic parameter guaranteed to have the same in memory representation at the same time without the manually drop. I believe that to be true. If certainly if that's not true, this is not safe. Let's go check. Manually drop T is subject to the same layout optimizations as T. It has no effect on the assumptions the compiler makes about these contents. So it should be. I think putting the type that we transferred into after the let inner would be a lot cleaner. Yeah, you're probably right. We can do that instead. So that would be this. Oh, no. Yeah, that works too. That's arguably cleaner. Make sure that no methods below drop values since we're only operating on the first instance of each value, the first shallow copy of each value. And then safety manually drop T has the same layout as T. And then for absorb second down here, this one is just going to take a mute self just the way that it is and other which is going to be self. And I'm guessing that all of the all the complaints here is that I don't have a hasher. So let's just go ahead and get that. Is it not dot hasher dot data dot hasher and we'll do the same thing here. And here let inner itself. But yeah, just minimize the diff. Technically, I could now use self here rather than inner, but but rather keep the diff small if we can. No methods. Yeah, you're right. All right. So we only have 68 errors to go. That seems good. So right handle now is really just a wrapper around this handle business. So maybe what we do here is I don't think we can do the first and second optimization actually. I'll show that in a second. But this is really just going to be handle now because it doesn't have all these fields. And I'll leave those commented out for now because they might come later. Creating a new right handle no longer require epics. In fact, creating a new right handle now all it requires is a left right right handle over an operation kv. And I think the other thing we want is just like before, we probably want this to just be a method on right handle. Oops, that's too much. So we'll just stick that up here. So that's going to take a w handle and return a self. It doesn't have to have all these where bounds because they're already on the input block. And all it really needs to do is stick in the handle there. So remember the handle now, the right handle here is really just a super thin wrapper around around the left right right handle that provides nicer access to inserting things into the off log. That's all it really does. Can look about rearrange the order of fields of inner versus inner whatever. So even if manually drop these v the inner type is not the same. They shouldn't be they should be they should be subject to the same layout restrictions. So they should be laid out exactly the same. Now, we don't need weight anymore. And refresh, we do kind of need refresh. In the sense that we just want to call self handle refresh. So it needs to do. Similarly with flush, we probably want to expose that to our collars. Remember, we probably don't want this to D ref mute into the inner right handle, because that would expose methods like like handle op or append op. So we're just going to rewire these to redirect inside. This is a why is this written as a mem swap? That's real weird. So I guess we want another off log here, which is like set meta M. And that also Oh, no. This means that the Oh, there's so much documentation. This means that we need an M here now as well, which means that anywhere that takes an operation now has to include the M in there. It's like the hell of generic parameters. Where is there we go? Absorb, absorb, absorb. And this now also has a, I guess, operation set meta, which takes the meta and unsurprisingly sets the meta to be that. And similarly, down here, great. Now apply first is this is probably going to complain because when we get a set meta operation, I think we already have M implements copy or something M implements clone. So when apply first is called, right, we only get a mutable reference to the operation. We don't get to take ownership of it, which means you only have a mutable reference to the meta as well. So we're going to have to clone that out here. I think I don't know. It's probably, it doesn't, there are too many compile errors for it to realize this particular area yet. But yeah, all the, the application of the operational log stays the same. Let's see what else we have up here. So we've add op, which used to be the same thing as sort of append op, like we have in left, right, right handle. And this is where the first optimization came in where if you haven't refreshed yet, you, you know that there are no readers in your right handle because you just created the map. So you can just do the operations directly into the map. The reason this opera optimization was helpful was because when you first create a type, chances are you want to like dump a bunch of stuff into it. And it's really annoying if you have, if those all have to go through the op log when it's not technically necessary. We might be able to fix this. So I guess this would be self handle append op. And otherwise, we have to do some real sneaky, I don't think we're going to be allowed to do this because without changing left, right. So basically, here, we want to get a mutable reference directly to the inner. But all that left, right, provides us with is the, the sort of it's an apply op that you get a mutable reference to the inner because that's when it can guarantee that there are no readers there. And apart from that, all it ever gives us is an immutable reference to the read side. Whereas here, what we're trying to get is a mutable reference to the right side. And left, right has no way to promise that that is safe to do. So it doesn't provide that method. But if we wanted to enable this optimization, what we would have to do is have like an unsafe, actually, it doesn't need to be unsafe. What we would have is a method on left, right handle that is specifically a raw right handle. It takes a mutable reference to self and returns a mutable pointer to T. And the reason we want this to be a pointer is because we can give out a pointer and then the caller has to like, in order to make that immutable reference, they can actually use for something, they have to write an unsafe block. It's not unsafe for us to give out the pointer, but it is unsafe for them to try to mutate through it because there's no guarantee that there aren't any readers. But this is all that we really need for the optimization to work. So this is just going to be, I guess, let's see here. I think it's just going to be this. Now, why is this unwrap okay? This unwrap is okay because right handle is only null after drop. I don't know why it even is an option. Like, why is W handle an option? Like, do we ever take it? I don't think we do. Oh, we take it here. I don't think there's a good reason for us to do this. So I think what we, so the reason for the option here is because we're using box into raw to get the pointer from the right handle. I feel like this is something I fixed on a branch. This doesn't actually need to do that. But that's fine. It's just a little awkward. This is something we could fix later. The W handle will only ever be none like between here and here, which is when we do the swap. So it should basically never be none observed externally from weight. I'm pretty sure we could rewrite the code to not have that be an option, but I'm going to not meddle too much with that code at the moment and have it work this way instead. And this is going to be a manually drop T, I think. Why is this, all right. All right. This could also, this is need to include the manually drop. I don't think that's an important detail. So this we can do as star mute manually drop T as star mute. Oh, that's real ugly rust format. Why you do this to me? Okay. So now the sort of add op on so that add op that we have on the ev map right handle, I think can now do this optimization, which is it grabs the handle. Why does it need to get the hasher from the read handle? That I mean, that's fine, I guess. But yeah, but I don't have to enter here either. I feel like this could use the handle from either side to be honest. Yeah, I think this can use the hasher from either side. So, so this is just going to be hasher is going to be actually it is going to we're going to make this into a reference here. So the reason why this is safe is because we know there are no outstanding readers since we haven't refreshed ever before. And then the hasher can now be w inner dot hasher. I guess data dot hasher. And now it's going to complain about other things, which is going to be annoying. Uh, we don't need do drop anymore. This is just going to work. But getting the hasher is going to be a little bit annoying, because we're going to do basically this right, apply second, absorb, apply second w hasher this. Oh, right. I need to actually use that left, right absorb. We need to use the trait in order to call the trait. That's fine. And I guess now where we implement absorb, we don't need to name it. This is absorb second. It takes a w inner and a hasher. But the right. Right. So this is going to complain because Oh, actually, I guess it actually wants the the data. But we can't actually do that because the R handle, I guess, because the borrow checker is going to kick and scream here saying that we're who, right, um, that we're accessing w inner both mutably and immutably. And it's right, right? We're calling absorbed second, which requires immutable reference to the inner. But then we're trying to also give an immutable reference to the same thing here, which is clearly also not okay. We could call enter here. Um, that's like a cheap enough, like operation anyway. So we could call uh, R handle dot enter. So not R. Oh, uh, self dot handle dot enter, uh, dot expect, um, map has not yet been destroyed. This does mean that we're going to end up like taking an epic, but taking an epic here is basically free because there we are the only writer and we're not waiting. Um, let's see. Um, okay. So now we have the, the self first operation and the self first operation here. Um, optimization, I mean, uh, actually seems to work, right? We get a, we, we happen to know here that there haven't been any refreshes. And therefore we do have exclusive access to the right handle. Uh, and that means that we can just directly absorb the operation rather than add it to the op log, just absorb it directly into W inner and then not store in the up log. Um, of course, as this note point out too, because we did this, the, the next time we try to apply operations like basically after the next refresh, um, the op log is going to be empty. And so we're going to have to have, we're going to have to clone the right handle into the read handle, uh, on the next refresh, because there's no op log to a copy over because we didn't add this operations to the op log. There is the reason this is if not self first, um, someone asked in chat is because this is, um, this is just the, the sort of happy path or the common path rather. It's not the happy path, but the common path. That's why I put it first. Um, I see there's a bunch of discussion of whether, whether it's okay, the transmute I'm doing, uh, transmute, whether it's okay for me to transmute a, uh, inner KVMS into an inner K manually drop VMS. Um, so, okay, to take it quick aside, um, the reason this discussion arises is because V and manually drop V are guaranteed to have the same layout of by the compiler. We know this from manually drop. The question is whether like some type V, some type generic over V is guaranteed to have the same layout as some type generic over manually drop V. Um, I'd be very surprised if it wasn't, but you're right that I don't know. So let me, let's see. So what does the Nomecon say about this? So the Nomecon is a really great way to look up like basically stuff about, uh, any unsafe code. Um, yeah. So Rust does not guarantee that an instance of A in this case has the same field ordering or padding as an instance of B, even though they have the same fields in the same order. The question is whether this applies to generics as well. This is not quite the same. Um, yeah. So it doesn't really say, right? So, okay, this example indicates that maybe they could be different, but it's not really the same case. The case we have here is like struct A generic over T, uh, and then is A of I 32 the same as A of manually drop I 32, which happens to have the same layout. I don't know. Um, it is true that we rely on that here and that may not be safe to rely on. Um, I wonder if unsafe code, code guidelines. So there's, um, there's a rustling, uh, working group called the unsafe code guidelines working group. And I think this is the kind of stuff that they've been looking at. So let's see what they say here. Um, that's not quite the same. Um, that's also not quite the same. Uh, repertransparent. Let's see if they have any discussion of repertransparent. No, that's not quite the same. That's also not quite the same. All the infields are single. Oh, that's similar, but not quite the same. Um, also not quite the same. I guess we want repertransparent generic. Uh, representation of structs. Yeah. So clearly a lot of discussion. I don't know what the conclusion was. Let's go down here to only reordering is still mandating greedy's padding. That would certainly help. Um, oh, collection transmute. Let's see what that says. This is like diving pretty deep into, um, technically with generics, you could even have different sizes. Yeah, but it's specifically the case when we know that the layout is the same because it's repertransparent. Um, that seems, yeah, that's not what we want. Um, all right, let's see what this chapter says. Representations of structs. All right. And let's look at this from master just in case. This is like diving pretty deep, but it's important. Oh, it's not on master. That's good. Uh, reference, reference, source, layout, structs and tuples. All right. The statement here are not yet guaranteed until an RFC. Um, struct types, just as a layout, tuples, that's tuple structs. I don't really care about those. Um, the layout of these fields themselves is already entirely determined by their types. And since we intend to allow creating references to fields, structs do not have any wiggle room here. Uh-huh. All right. Default layout. Default layout is not specified. I've not yet reached a full consensus. Um, not required to select the same line across compilations. That's fine. Two structs with the same field types may not be laid out in the same way. Naturally, in practice, the compiler aims to produce deterministic output. As the, as of the time of this writing, the compiler will reorder struct fields to minimize the overall size of the struct. So that suggests that currently at least this is fine because the layout is the same. Um, this seems like a good, I guess it's an unresolved question. Um, yeah, it's sort of like deterministic layout, I guess is sort of what we're after. Yeah. Like this is a good question, right? Uh, whether it's deterministic, only dependent on certain properties of the fields. Like if the, if all the, all that matters is the size and layout of each field, then our two things should be the same. Um, but yeah, it seems appealing. Yeah. So here's an example of like substitution might be worth for us to like chime in on this discussion, but I don't know. Yeah. So here's right. Like I suggest rep or transparent equivalent would still be enough. Uh, all right. So it sounds like we need to chime in on this discussion to move that forward. Um, we could make this rep or C, but I don't really want to make it rep or C. So I'm going to keep it the way it is. And then I'm going to add a comment to this discussion after the stream finishes. But I think it's fine for now. All right. Um, okay. So now we have the first optimization, which is that if we haven't swapped yet, then we just do the modification directly to the map because we know that no one else has it. And then what we need to deal with is, um, if we're trying to refresh, uh, and, uh, we can see this in, so remember I changed refresh to be, to just call the inner handle refresh. Um, but in reality, that's not quite what we want. Uh, so we're going to do source write RS. So it used to be that refresh did, uh, where is it here? It did a wait and after the wait did a second. That's going to be a problem. Ah, I know what we can do. Okay. I think, I think we can still do this. So, uh, okay. This is going to be a little bit ugly, but I think it's going to work, which is that, um, we're actually going to, uh, so what I was thinking here was that we could append an operation that's like, uh, do, do, uh, do the second pass of copying over, but it might just be a pain kind of tempted to get it to work, but I think it's also going to be a lot of pain. Um, let's right. So what I was thinking here, right, was self handle, append off, um, append off like operation, um, swap in read, uh, just clone maybe. Um, um, so that would be one way to do it. It might mean that we end up with an op log that has a lot of these operations. Uh, but I guess we could, we could get around that. Okay. This is going to be, this is a little ridiculous, but, um, so I guess maybe, maybe we just do like, you know, optimization step. So we can either be no refresh. Cued clone is bull. Or it can be, um, um, or it can be the right, uh, right side empty, or it can be none. So I guess I, I decided to go against myself in, uh, in saying that I would not change things around, uh, position. All right. So the idea here is going to be that if, oh, drop for right handle can now go away. That's really nice. Um, so this handle is going to start out with the optimization being optimization, no refresh has happened yet. And we have not queued a clone yet. Um, we're going to match here on self.optimization. Uh, if we're in optimization, no refresh, then whatever. If we're in optimization, right side empty, then whatever. Uh, and if we're in optimization, whatever. Um, I think that's going to happen after the refresh. Uh, so this is just going to move the optimization forward. Uh, optimization, right side empty. Optimization none. And optimization none. Uh, if, right. So if we're in the case where optimization, where we haven't done a refray, uh, a refresh yet, optimization, then then we're going to just do the, if, if a refresh hasn't happened yet, we know that we own the right handles, we're just going to do the modification directly to the right handle. Uh, if we haven't queued a clone, then we need to queue, uh, a clone operation. The reason for this is if we didn't do this, absorb wouldn't be called for our operation. Um, and therefore the, the copy would never happen. We needed it to be at least one operation in the Oplog that tells us that we need to pull in everything from the read handle into the right handle. Um, but we only want to do that once. We don't want the Oplog to fill up with tons and tons of these things. Um, and if we're not in that optimization state, we're just going to, we're just going to append the, the operation to the queue as normal. So we're going to have here a just clone our handle. Um, and what is this going to look like? Well, in apply or absorb first, I guess, if the operation we get is just clone our handle, what does that mean? Um, so, so apply first is going to be applied to the right handle that we have already modified. So we're going to do nothing. Uh, this is applying the operation to the right handle, to the original right handle, just, uh, she's that true. Yeah, to the original right handle, um, which we already applied the, uh, first batch of operations to if we're told to apply it second, that means that now, uh, we're applying the, the, the, this means that it's the first refresh that's happened and the right handle is the original read handle, uh, which is empty and needs to copy over all data from the, uh, right handle that we wrote to directly. So in this case, inner dot data is just going to be, um, I guess dot extend. Uh, well, is that even really going to work? That may not work well. What did it used to do on second? It used to get the R handle. Yeah, it used to have this comment in it. So really, it would be nice if we could just clone here, but remember the problem around hashing. We need to make sure that the, uh, we need to make sure that the any bucket, any, um, like value sets, like a created in the, in the map all use the same hasher, which means that we need to pass in a hasher, um, which means that we can't just clone over the whole map. Actually, maybe we could clone over, I feel like we might be able to clone the whole map, but I'm going to not risk that. So we have the R handle, don't we? Oh, I guess it's called other like so. And who knows why this is complaining type V cannot be dereferenced. That's, uh, that looks ugly. Oh, it's, um, it's like a manually drop challenge here, which is values from the, this is going to be a manually drop new of that. I think a little unclear, right? So inner here is a thing that holds Vs. So why can't I give it Vs? Values manually drop. Oh, I see this gives me back the shallow copy here gives me a manually drop. And I need to make those not be manually drop somehow. So into inner into, all right, this is actually manually drop into inner that thing. Yeah. And then I think actually we don't even need this, this right side empty thing. So really this is just an option. So this can be instead of calling it something grandiose like optimization, it can be called, can be called just direct right. So initially direct right is going to be some and the inner value is false to indicate that the, I guess we should document that up here. If some right directly to the right handle map, since no refresh has happened. Some false indicates that no, that the necessary operation, just clone our handle has not yet been appended to the Oplog for when a refresh does happen. And this can now just do take a factor doesn't even have to do that. It can just set that equal to none. Once we've done a refresh, we can no longer do direct rights. And now we can say if let some is direct right, then we do that. Can you replace, can you replace the star V with the as ref? Probably. I don't know if that's nicer, but it has to be D ref, which means I have to use a trade, which seems not worth it. Alright, where's our now? Inner with capacity, I see is manually drop. So I think what we want is for our we don't have, we don't need do drop anymore. Don't use that. These I think is actually going to do just this expected read read handle. So where's our new? Why does it complain about this thing? Oh, it's from I can just be new instead. New seems fine. It's a private function anyway. All right. So now we're getting, we've gotten pretty far, right? So now right doesn't really have any stuff in it about, um, uh, like refreshing or, uh, or like any of the epic stuff, like none of that is in right anymore. Now it's only just tons and tons and tons of, uh, tons and tons of, and tons of, um, map specific operations. Why does this even need to D ref? Oh, I think this is going to be annoying. That's going to be a left, right read handle of inner of that. All right. So now we need to get to the, oh, all right, the, it's complaining about the tests. That's fine. So let's look at the read side now. Uh, read should also be a lot simpler now. Factory goes away. That's great. Uh, read handle should now only really have a, uh, handle, which is going to be a left, right read handle. And again, to an inner of that. Um, and it shouldn't really have anything else. Uh, we don't have to implement drop specifically anymore. Uh, which also means that we don't need to have these anymore. Oh, we do for inner to make sense. Um, which means that this implementation is now just handle. Uh, we can actually just derive clone, but we're not going to, the reason we're not going to drive clone is we, uh, we don't need, so if you would derive clone, it's also going to say that KV M and S need to implement clone. Uh, we don't really care about that. Um, so we are just going to do, uh, self handle is self handle clone. Uh, what else do we got? This thing is now going to go into read handle. Uh, so this is going to be a pub create new, that's going to take a handle, which is going to be a left, right handle again, no epics. And really it's just going to write wrap self with that handle. And again, the only purpose really of us having a wrapper type here is, um, we have the wrapper type so that, um, we can provide map specific methods on this. It's no longer a handle that can go away. That's all handled by left, right. Um, hasher, we no longer need in here. Um, I guess we can still have read, but I don't kind of want to call this enter again. I guess if we're doing a breaking release anyway, we might as well. Um, and that's going to give me an enter and that's going to create a map redreff. That's fine. Um, this I guess is going to be enter, enter, enter, all of these are just going to be enter. Right. I need to, uh, that's just because map, we haven't fixed up mat redreff yet. Um, this is just going to be mapped, but it's actually going to be left. I'll have to deal with that in a second. That's going to be mad read map redreff map. Once we're done with it, do we even use, right? Actually, that's going to be a read guard. Oh, I see. So read is actually a little different. Um, read gives you, okay. So, so in the, in EV map side of things, we actually have two kinds of guards. We have a read guard and then we have a, uh, map redreff and map redreff is basically a read guard specifically for the map. Um, I don't know if we need this distinction anymore. I think map redreff is all you really need. I see read guard is actually used for M, but I think this can just be a left, right read guard and no one's going to care. Um, and this is going to be this. Um, that's fine. This is going to be left, right read guard. So that means that we're not even going to have a read guard here. I don't think like, what does our read guard do? Yeah, nothing really nothing that map redreff couldn't do. So I think we're not going to have a guard. We're just going to have redreff, uh, which means that we can RM source read, uh, guard. Um, I think I found proof that manually drop transmute is on sound. All right, let's check this out. This is why it's nice to have chat because people can check things up while I'm coding. Guaranteed to have the same size of linemen at ABI. This is for maybe on in it, not, um, from any bit values. So this is not quite the same. It's similar. I mean that this first sentence, right, is pointing out specifically what we saw on the other thread that there's no formal guaranteed that they're the same, but in practice, the compiler currently treats them the same. They go on here to say that specifically because any bit value is valid for maybe on in it, the compiler can't apply, um, optimizations. That's not true for, um, made, uh, manually drop for manually drop. It has the same validity requirements as maybe on in it. Um, you can't stick like if you have a manually drop pool, you can't stick like arbitrary bit values in the pool. Um, for maybe on in it, you can stick any bit value you want in that pool. Um, so it can the compiler can optimize manually drop just like it optimizes, um, uh, manually, it can optimize manually drop T the same way as it optimizes T. So I think that should be fine. But you are right that this is something that's clearly not guaranteed by the compiler, but it seems to be what it currently does. And that's why I want to follow up on this, um, on this thread specifically pointing out this issue and see what they say. It's true. We could just rep or see it, um, rep or see as some other implications are kind of sad, but maybe that's worthwhile. I think for now, I want to keep it and then point out the issue. Um, but I mean, it's a good, it's a good problem to point out for sure. Uh, all right. So we can tidy up this quite a bit. Don't need men many more. That's always a good sign. Um, so this is, I guess this is a left, right, read guard. We can reuse the left, right, read guards because all they're really doing is they D ref into the inner type. We might be able, we might want to re export left, right, read guard. Um, that might be good. Uh, I really want, oh, I see this should be self handle enter. That's what we want. This should be self handle enter. This should be handle enter. Um, this one's trickier. This one is like, if inner is none, then none, uh, else, uh, inner dot, left, right, read guard, map, inner, uh, as ref unwrap, data, get key. Why would this be? Oh, right. Um, yeah. So this one's a little awkward. This is why it had map opt, which is it's not that we need to do this. It's that inner dot data dot get of key. What this is going to return is a read guard left, right, read guard of option like V. But what we want to return from the function is a option read guard V. Um, it's not really a nice way for us to do that. Actually, maybe there, yeah. So this is why I think, I think we can't do this with a normal map. We're going to have to basically do, uh, let value is this. So we're going to map out the data for the key. And if the value is none, then that otherwise we have to map again to value, uh, value, uh, inner dot unwrap. What am I doing something silly? There we go. Mismatch types. I was worried about that. Yeah, we might actually just need map opt. There might not be a nice way to do this without map opt, which is, okay. So the problem here is that get returns an option of a reference, uh, but map requires that we return, um, a ref. Actually, why does it require that we turn a reference? Can we just fix that instead? Like if map was not required to return that, but just did this, um, um, and now I do want you to be sized. Is that fine? Oh, it's cause this internally stores a T, doesn't it? Uh, no, this is probably going to be weird. See, that's a good question. I wonder what they do for, uh, ref mute, for example. So what's the signature for ref mute? Yeah. So they do the same thing. They do the same thing this used to do. Um, and then the map that they provide is the same type of map that we provide, but I think that means you can't really do options, which is pretty awkward. Um, like we could always return this, but that doesn't seem quite right either. So I think we do actually want a map opt here, which is kind of silly, but, um, oh, actually, maybe we don't, maybe I'm just being silly. So this gives us something that D refs. Uh, another problem is we can't, this gives us an option ref, but we have to return a ref itself, uh, which won't work here. Um, so I think we do need a map opt or I guess a try map. Although try should really be a result. If we had the try trade, that'd be great, but we don't. Uh, so this is going to have to be a option you. Um, and this is going to be that and this is going to be this and it returns an option read guard. So now we're going to just do try map of that and it complains because the value is manually drop. It doesn't have to be because we're giving a reference to it. Who can't you write to a mutable option variable in the outer scope in the map and return in the map? Um, I don't think you can because we want to, um, because we actually have to return the guard. Like we had, it's we're returning a guarded value. We don't want to, we don't want to clone out the value or anything. Right. We want to return a guarded value, which is basically a guarded reference. Um, so we can't move. We, like if we, if we did like, uh, Val is none. And then we did like a map where we set val equals this, that wouldn't work because the, the return, the thing that we stick inside val is tied to the lifetime of inner. So if we tried to return val, it would say this has, this lives as long as the inner does and we're not allowed to drop inner. Think of it as like, if we returned the, if we tried to return just the reference, that would mean that we dropped the guard, which releases the epic, which means we now have a pointer into the map, but we've also released the epic. So the writer might overwrite that value. So that won't, I don't think that will work. Um, what else do we have? We have left, right read guard. And this presumably does a try map as well. Right. Uh, we called this read guard, try map. Fantastic. Uh, and this now it doesn't have to do that anymore. Wait, did I, I did something stupid here tonight? What did I do that was stupid? Oh, I see what I did. I think I need to do read guard, try map, read guard, try map of this thing. I mean, I thought that's what I did. I mean, clearly I'm doing something silly here, but what? So get raw returns an option. So what we get back here is a read guard to values. Like just to see if I'm being silly, if I do this, does it work? Oh, I'm entirely being stupid. Uh, the old version here uses map ref, not try map. So really I'm just being silly and this needs to be map. This just needs to be map with this. Uh, I thought, and apparently I was wrong. Why is this given a manually drop? I've definitely been doing something strange here with I have a manually drop somewhere where I shouldn't have a manually drop. And I think I have it. I think I have it. Oh, this expects to take a manually drop. I see. But there's no reason to remove the manually drop anymore. So that can just go away, which means that this can go away. Great. Get one left, right, read guard. This, this one is a map off. Okay. So left, right, read guard. Really, I should give, I should use this to avoid having to type it each time. Try map of that with this, except it no longer has to be user friendly. And it does the right thing. So so far, like this transition is going pretty nicely, I think. Like these are mostly mechanical changes because the types changed. Oops. Left, right, read, handle, try, try map of inner with this thing. And this map is no longer necessary read guard is destroyed is just handle dot is destroyed. This does a, this should be an enter. That's probably fine. This should be a notice the normal map. This should be an enter. All right. So now we need map read ref, which is at least in theory, the last thing that we need. No read guard. That's true. Oh, there's no read guard. So what am I even doing? I can just use left, right, read guard, left, right, read guard is just going to be read guard. Because there is no left, there's no read guard redefined in EV map now. That's nice. So this can now use left, right, read guard as well. This holds a V read guard iter gets pretty close except it doesn't need manually drop anymore. Doesn't need to call user friendly anymore. User friendly. So user friendly is just like a helper type that maps from things that are manually dropped to things that are just removed the manually drop. And that's because all of all the read stuff only operates on on like really like shared references anyway. So there's nothing really for them to deal with. Yeah, what else we got? We're getting pretty close, I think. Right RS 121. That's in 121 set meta. So that should just be add op operation set meta meta. Why does this return M? Oh, it used to return the old meta, even though that's not really documented. That's awkward. We're gonna have it not do that. Although I did realize that. Yeah, no, that seems great. All right, what else we got? We've got something here in lib right new compile. So close read guard is private. Yeah, we're no longer going to expose read guard. I guess we might want to yeah, we'll pub use left right read guard. Almost compiles read. And I also want to do the same change we did here of moving read mod to read dot RS. So read, read RS line 176 where we got here, cannot borrow self handles mutable. Right, so this is definitely one of the big annoyances right that now with a read handle, all these methods now need to take mute self. Like get one, it's going to take a mutable reference to self get is going to take a mutable reference to self get raw like this is what I was sort of trying to avoid with having with having read handle not be sync and just having it do an immutable reference to self instead. In practice, it probably doesn't make too much of a difference because you couldn't alias a read guard anyway. So I guess we'll just have to keep this the way it was. But yeah, it is a little weird that a read handle requires you to have a mutable reference to self to call a given method. We're getting somewhere right 322. Oh, where is it trying to move it's trying to move down on 405. I love the compiler. This is going to be rough. And it's like, Oh, you're trying to move out of this thing because here. Nice. Right 134. Cannot borrow self handle as mutable. Right, we're going to want to do this actually cannot borrow as mutable self dot handle. So the right handle only gives us an immutable reference to its read guard to its read handle. But now that that all takes mutable references that has to implement D ref mute. So down here, we also have to implement D ref mute. Which is fine. There's nothing really preventing us from having that be safe. No, the other right 549. So this is the downside of us doing all this wrapping is that it's not clear how we provide reads through the right handle in ev map, because it only holds a right handle. And the right handle gives us access to a left right read handle, but it doesn't give us access to a ev map read handle. We could construct an ev map read handle, but then we wouldn't match the D ref trade. One option here is to store our read handle ourselves here. But that seems ugly too. Hmm. So do you see the problem here? So we have the, let me go back up the definition. So the right handle and ev map, all it really stores is a left, right, right handle. However, we would like for it to be possible if you have a right handle to call read handle methods on the right handle. And the way we did this before was that right handle with D ref into a read handle. Unfortunately, in our case, the while we have a right handle, D ref of this right handle will give us a left, right, right handle, not an ev map, right handle, which means that the only methods you can call are the methods on a left, right read handle, which is really just like accessing the, like it's really just calling enter, which only gives you the non map specific methods, which is kind of sad. We don't really have a way to give you a ev map read handle from a right handle. We could provide you with a method that's like call this and it will create a read handle for you that wraps the inner one. But it's not very nice to work with. We need an enter on right handle itself. Like the other way to do this, right, is to have an R handle stored in the right handle, which is like a proper read handle like this. It's a little unfortunate. It means we have an extra epic lying around. But it seems nicer than having to provide this like enter functionality. Wait, why? Why does Oh, I'm being silly. Whoa. Oh, right. Yeah. So with this, we can now have both D ref and D ref mute into a read handle at the cost of storing sort of unnecessary extra epic. So this is now going to target a read handle. And this is going to give you a mutable read handle. And it doesn't even have to go through D ref. And now we're going to have to make sure that we construct that. These are all in tests. I don't care about tests yet. Line 93. So this is going to have to do an R handle, which is going to be a it's going to be a read handle. How's this even going to work? It's going to be a read handle new. Yeah, read handle new from handle dot from left right read handle clone of the referencing handle, I think. Okay, that was a little bit of a magical incantation. But what we want to do is we want to construct a new EV map read handle that we can stick inside our EV map right handle. And the way we're going to do that is we're going to take the left, right, right handle, dereference it. So we get a left, right, read handle, clone that left, right, read handle. So now we have a an owned left, right, read handle. And then we construct a new EV map read handle from that. The other way we could go about doing this is we could have like map the map methods of read handle be like a trait that we implement for left, right, read handle. That would also work. But it would mean that people have to import the trait in order to call any of our methods. It would be a little annoying. Yeah, it's, it's annoying that right and right are pronounced the same in English, and that the opposite of right is left and also wrong. It's a little annoying. What do we need to clone? We need to clone because in order to construct a read handle, like an EV map read handle, an EV map read handle is a wrapper around a left, right, read handle, but it's a wrapper around an owned left, right, read handle. And the, the one that we can only get shared access to the one or mutable access, but we can't own the one that's already in the right handle. We can't extract it. So we have to clone it. But this is cheap. Like this is just when you initially construct the right handle. So this is not an operation you do like all the time. It compiles. Okay, so there's no longer a use for user friendly. That's good. Right. No longer needs a bunch of its imports. That's good. Values no longer needs manually drop. Inner no longer needs manually drop. Lib no longer needs a bunch of things that are imported up here. That's good. Love the leading code. Okay. So now we have EV map compiling as well. The tests will not probably not compile yet because they rely on access to private fields in what are now in left, right. But let's see. Let's do a cargo test. Let's do a lib test first. Yes, you see, it depends on this refreshes field and on being able to look directly into the op log. And neither of these are things that we currently have access to 583 we got here. That should be enter. Now, I think realistically, these tests probably all need to be on left, right instead. So if we take and grab all of these, we're going to left, right, right. And actually, why not? All right. So we're going to do something wonky. We're going to do config tests. We're going to impel absorb. We're going to have a enum counter operations. It's going to have add and subtract. And so it's going to have for now, absorb counter operation, counter operation, T for you have a struct counter, also T, which just holds a T for actually, I don't want this to be generic. What am I doing? That seems entirely unnecessary. It's just going to be for testing anyway. So we're going to impel counter operate absorb counter operation for I 32. Oh, we haven't implemented. We haven't implemented clear the drop first. We're going to have to do that actually. But give me a second before I do that. Actually, let's do that right now before I forget. So remember, there's a drop first method. It used to be that that's just called clear. So let's look at what clear does. Well, clear just adds the operation clear. I feel like that probably didn't work in the past because no, it called clear on the map itself. And that's what we'll want to do here too, which is for drop first, we're going to want to do inner dot clear like this, that we don't drop values since we're only dropping the first shallow copy of each value. So actually drop first is pretty straightforward, right? We again pull the trick where we transmute into manually drop so that when we call clear, clear is not actually going to drop any of the values. But it is going to like, well, actually, maybe drop first should not take a mute self, but a self. But it's actually going to be hard to do that, I think, because where we call it, we only have a mute self. This is going to be really annoying. It's going to be really annoying. Actually, we don't need, well, let me try to explain what's going on. So when we drop a right handle, we now have two maps, one and they have sort of clones of each other's data. So we need to forget from one and drop from the other. We need to have a mechanism whereby we I hear in my mouth. That was really annoying. We have to somehow tell the inner type, the absorb type that like drop all the things in this thing. Unfortunately, we don't really have a good way of doing so because all we can give it is a mutable reference to itself. The reason for that is if we look at right and the application of drop, the right handle, all we have is a mutable reference to it. I guess actually we can make this be owned. Come to think of it because we've we've swapped it out. We set it to null and we wait. Yeah, so we actually do know that so we can actually take an unwrap here. And that way, that way we can actually drop first. Yeah, so I think we want drop first to actually take self so that it can so that it can actually drop the values safely. So here drop first is going to take self. That's going to dereference self. And and now over an even up right, this is now going to be given self as we're going to cast self to that type. We could even if we wanted to hear and maybe that is a good idea is to say that this actually gets a box of self. Do we really want to forward through box here? It seems dumb, but yeah, I guess we do. Given that we're only doing this for box, we don't really need the macro anymore. This star is going to get weird for any type that's not box. So I think we're actually going to just do this straightforwardly. Unsafe, Imple, T for box T where T is absorbed that. And now what we do is we transmute it to have manually drop and then we don't even have to do anything. We can just drop inner. We don't have to clear it first because the values already marked as manually drop. So notice that we could also unsafe manually drop drop in where we call drop first. I don't think that would work because remember we want to we want to actually run the destructor for the type itself. It's just there might be alias stuff inside of the type, like the values in the map that are aliased that we don't want to drop. All right. So now back to what we were doing for testing, which is yeah, I guess really this should be a manually drop box, not the other way around. Oh, manually drop into inner. Then I'm sure there are other areas that resulted as a part of that. Yeah. Why does this not expected reference box T? Oh, this, ignore this. It's a silly error. Box into raw. I really want this to not be an inside of the weight function. We really shouldn't be like doing into raw. It seems like it's probably a mistake. Like it's not actually a problem. It's just like a recipe for shooting yourself in the foot. Um, because here we, well, this means we have an owned, an owned box, which means that if we happen to panic or something in here, then we're going to drop that box and things are going to be said. Um, well, I guess actually we can do manually drop of this. Um, why is this being annoying? Oh, this is doing into raw. What am I doing? I'm being, I think I've been streaming for too long. This is not doing from rods doing into raw. This is the part that should be a manually drop new and this should not expected manually drop box T got manually drop box manually drop T that seems annoying. Why that should just be this is why. All right. What else we got casting mute box T as mute manually drop T that should be fine. Oh, that's awful. Oh man. Uh, right. This is now going to, uh, D ref into a mute box T and then I'm going to have to D ref it again. But I thought this would work, but apparently not. So W handle is a, uh, manually drop of box. It's an option manually drop box. Um, so I guess let's do as mute. So that gives me and then expect. So that gives me a mutable reference to an manually drop of a box. Uh, and then we need to D reference that twice, once to go through the three times, once to go through the mutable reference, once to go through the manually drop, once to go through the box and that a mute of that is a pointer to the actual T, uh, and then we cast that. Now that's obviously terrible. I mean, the signatures healthier, right? But it's really stupid. Um, but I'm surprised that the SD ref mute twice didn't work. Um, all right, I'm just going to leave that there because it makes me kind of happy, even though it doesn't censored swearing at the compiler. Yeah, you're not wrong. Um, doesn't star mute T implicitly cast to mute? Oh, it might. You might be right. I might be able to just do this. Yeah. Good call. Getting some implicit cast. Not that that really helped the readability that much, but you know, um, maybe a map, right? RS drop first. So miss something up here. Oh, that's in the tests. Okay. So let's go back and see if that test passes for here in left, right? Right. So I was in the middle of implementing this thing. Um, so this one is kind of funny. So the operation can be either a counter operation ad, uh, I, in which case we, we're going to do star self plus equals I, or we're going to do a subtract minus equals I. And would you believe it the implementation of absorb second is the same? Um, I guess technically this is an unsafe. I don't know if this needs to be an unsafe. I'm undecided whether that's necessary. And drop first does not have to do anything. Um, so as D rough mute twice shouldn't be the same problem as into into because, um, D ref has an associated type target. So there's no type inference. Uh, any type can only dereference into one type, which is not the same as, um, into. Oh, right. You're right. Um, because it's assigned, you can, because it's assigned, you can just add a negative number. It's true. I mean, this, to be honest, you know what, let's just, it doesn't matter that this is an enum like different types do not matter. So really we're just going to do this self plus equals operation dot zero. Great. Even better. And I guess this is now counter add off. Um, all right, so back to our right test. How about we have this test use, um, wait, is there a new in the root? I guess there is. Yeah. Um, pendop. Uh, I guess we're going to also use counter add up and then we're just going to counter add up 42. And then we're going to check, uh, that R, so R dot enter dot unwrap, we're going to assert that that is equal to, and here comes the magic 42. Nice. Um, and then we're going to pin it again. It's fine. Oh, uh, create that up. This doesn't need X, but this does need type annotations, which is going to be I 32 and an empty off log. Um, actually, this is a good point to talk about associated types. So you'll notice that the absorb trait is generic over O. Another alternative would be to have an associated type here. So we could say, uh, operation is equal to the type operation and then get rid of the O. Um, and there, these are both valid options. The decision comes down to, do you expect that people will want to implement the absorb trait using different operational types, but for the same type? Um, because if you expect people to implement absorb like my off type one for hash map and someone else to implement my, uh, off log type two, uh, for hash map. Um, if you do, then it has to be a generic type on the trait. If on the other end you expect any given type to only have one possible implementation of absorb, like an implementation of ha, uh, absorb for hash map will always use the same operational type. Then you make it an associated type. The, the, so, so having it be a generic type parameter is more, more generic. Like it means you will have more flexibility in what implementations you can have. The downside is that, um, you need to have new, for example, needs to be generic over T and O. If O was an associated type of T, we could get rid of this entirely. Right. And new would only take one type parameter, which is the type of the, of the type that implements absorb. Um, and so that, that's more ergonomic because it's constrained to, there is only one implementation. Um, this is sort of what I was getting at with D ref and into before that into is as a generic parameter, what type it can be turned into, which means that any given type can be turned into many different types. You can implement the into take trait multiple times for a given type. Um, D ref on the other hand does not D ref has an associated type target. And this means that, um, you can only D ref a given type into one other type. There's never an ambiguity. There are never multiple implementations. Um, uh, and this means that sometimes when you call into the compilers like, I don't know what you want me to turn into, like there are multiple options here and you need to tell me and you often need to give compiler annotations. Um, whereas that's not true for D ref. For D ref, you will never have to say what type to D ref into because there is only ever one choice. Um, for absorb, I don't know which case makes sense. Um, um, I guess it's probably unlikely that people will want to bring their own like operational type, but it's not entirely unreasonable, right? Uh, you can imagine that someone wants to support specific operations on like hash map when the key in value is their type. Um, so I'm inclined to leave it at O for now. Um, which does sadly mean that new needs to be generic over O, but at least now you know that that was a conscious decision. As that means that when we call new here, we have to specify the like comma underscore. The compiler can infer what O should be from what we passed to append op, but it can't infer what T should be because it can be any type that can absorb a counter add op, which as far as the compiler knows, there might be many. Yeah, it does look weird. Fine. That better. Um, let's see here. I guess this is going to be like a append op counter add op 42. Actually, that's why this was here, I think, so that this value when I check it down here. Oh, I mean, there's no real reason for this to be different than because we don't actually check the output value ever. Uh, uh, maybe the new function should have some information as to what it creates kind of like channel or remember normally you would use this as ev map, colon, colon new, right? Um, so that's, that's why it currently doesn't who 303 what I do here, right? Now the read handle has to be mutable. Okay, great. So the tests and left, right work, um, they don't do anything particularly interesting, but at least it checks that refreshes are happening. Um, the real slight big question is going to be around the ev map tests. So let's go to ev map read. Um, so these tests could, oh, some of these are very specific to ev map. Oh, I guess this one is very specific to the member. Um, this only needs to be mute. I think that should just work. So that's nice, right? So the ev map API has stayed almost exactly the same. Um, okay. So all the, the ev map, like unit tests, uh, succeed, they don't do anything pretty particularly interesting, but this is just, we didn't like majorly screw up somewhere. Um, now is where it gets really interesting, which is to run the full ev map test suite, and that's going to be much more of a pain. Um, I think many of these, so these are the, the library tests for ev map. And I think many of these, it makes a lot of sense for us to, uh, sort of port over into, um, port over into left, right, but we can keep them here from now. It doesn't, like because ev map hasn't really changed. Uh, it should be fairly trivial for us to port these over. So for example, uh, anything that has calls read should instead call enter. Um, anything that has r comma should probably be, oops. Uh, oh my, anything that has r, r comma should probably be mute r comma. And that's probably going to make most of the test compile already. This apparently doesn't need to be mutable. Um, this does have to be mutable. This does not have to be mutable, but the clone does. This doesn't have to be mutable, but the clone does. And yeah, so this is all ev map is also set up with quick test. Um, so that's probably going to account for most of the other compile differences, but the change here is fairly mechanical. 261 is probably another mutable thing. Yeah, this needs to be mute. What else we got? 223, this thing does not need to be, uh, it definitely does. What else we got? Uh, this is a mutable reference to the read handle now. Mutable reference to the read handle, mutable reference to the read handle. Oh, that's going to be bad. I'm going to skip. Oh, is that the only one? All right. What do we got here? A dot enter dot iter dot map. Um, so this is one example of where, um, it was kind of nice for read handle to only take an immutable, a shared reference because we could do things like enter, but then also call get. Um, which it won't really let us do anymore. Um, but to be honest, I don't know what this would even end up doing. I feel like this would end up doing something kind of stupid. The basic observation here is that if you call enter, you should be allowed to call enter again, right? There's nothing really preventing you from doing that, but in the current API, we are restricting the caller from doing this. Um, if they call enter and then, then that, that takes a mutable reference to self and it ties the lifetime of that mutable borrow into the read guard that it returns, which means that you can't then borrow again. Um, but what we might be able to do is guard is a dot enter guard, iter and guard, get, uh, enter unwrap guard. So this, I think this is guard dot keys and don't know why there's a flatten. Great. But actually that's not too bad. I guess we are at like the six hour mark, huh? Um, quick test generates like random test inputs. Yeah. Uh, what's the meaning of M when you used percent S M, uh, magic. Uh, this makes, this makes Vim regular expressions, uh, interpret more things as special characters. I should really make that alias be using very magic instead, but which is V. Oh, it does pretty well. Operation string did something weird, like 146. Of course, of course that's the one we just did. Um, uh, right. So this is going to be if let some guard is this, then, uh, guard else. As he's in the past, this would actually here we want to assert. Uh, I see. So really what this is saying is that it failed to enter, which means the map has been destroyed. Um, but it's already checked that the, the reference map and the, the current map are the same length. So really if this is destroyed, that means we've already done the testing. We need reference was empty. Read handle was destroyed. So all is well, maybe return true. Something failed. Oh, these are doc tests. That is a good sign when only the doc tests are failing. Yeah, this needs to be mute. Hmm. Oh, I'm almost positive. Some of this is going to be bad. This is going to be mute. Actually, this is not going to be mute. This is going to be mute. This is going to be mute. This is no longer true. Read handle is sync. Nice. I guess let's do doc. Um, the, the read guard didn't need mute self before because it was using atomics internally in itself. Um, to allow it to just take ref self. The thing is it doesn't actually need mute self. Um, because most of what it's doing is just operating on the epic counter anyway. Um, which is, which is anyway shared, like you need to use atomics for it regardless. Um, but it's sort of like hiding the fact that really it's mutating some state. It's just, it's a little misleading, right? Cause when you see a read handle take mute self, you think that it mutates the map, but it's, it has an immutable reference to the map. It's just as a mutable reference to its own internal state, like the handle state. Um, which is a little subtle. All right, what do we got? What else is failing? Book reviews are, that does need to be mutable. Uh, what else we got here? Right on line 32. That has to be mutable. 34. What? I mean 86. That's very misleading. Source Lib RS line 34. Source Lib RS line 86. I know it's because of doc comments having, this is line 34 of the example, but that's annoying. 86. Um, no method read for strike. This has to be enter. Nice. That's pretty promising. Let's run cargo, Miri test too. Oh, but it was, is Miri like not available these days? I know there's been like a bit of ups and downs. That's annoying. Uh, tool state. No, tool state. No, Miri is supposed to be building. Oh, it's missing on tonight's nightly. That's really annoying. Well, all right, that's fine. I'll run Miri later. I guess the last thing we'll do a cargo clippy here. Oh, clippy is just happy. Great. What about here? All right. This is pretty nice. I guess what I'll do is I'll go ahead and commit all of this. Did I read? Did I leave any to-dos? I did. Read. Left, right. Read. 126. Oh, we already fixed that to do. Great. I guess we might as well get, oh, EV map derive was using the wrong version, but that's fine. It's easy enough for us to fix, I think, because derive cargo, this can just be version 11.0.0 alpha.1. I think that's the version I've set. Yeah. Why not run Miri on stable? It wouldn't install for me on stable, but maybe I was doing something silly. This also says broken inter dock links. Oh, nightkin is in. The mutual reference feels kind of unorganomic and it's a very breaking change. Is it possible to use some unsafe magic to take an immutable reference? It is. The trick here is going to be to use a cell and then keep track of how many times we enter. I really don't want to deal with that right now. I don't know why. Oh, the failing one is, oh, the compiler has changed. Yeah, that's what we want to do. Great. Yeah. I mean, I think we can do that pretty easily, actually. If we really want to, we can do that. So that's going to be not guard, but all right. So, okay, we have two options here. One is to keep the handle as using taking immutable reference and that makes these things nice and it also allows read handle to be sync. The other alternative is to do a little bit of cell hacking here, which would make read not be sync, but would allow it to take immutable references. Self, that is. I mean, maybe it's worth it. A little out of a bunch of internal complexity, but maybe it's not too bad. Basically, what we have to keep track of is if you enter multiple times, you should still only reset the epic when the last thing gets, when the last guard gets dropped. Yeah, that's true. All right. So, let's do that. We might as well. So in that case, we're going to use cell cell and enters. Then what we're going to do is, so enters is going to be a count of the number of times enter has been called without the guard having been dropped. So whenever that goes, if we call enter, while that value is one or greater than we do nothing, we just give out a guard. And when we drop a guard, we check whether that value has reached zero and only if it reaches zero do we reset the epic. All right. So in that case, my epic is going to be cell new zero and enters is also going to be cell new zero. So let's see. If self enters is not zero, we have already locked the epic. Just give out another guard. So actually, this is tricky because I know we are preventing another swap. So this should be safe to do, which only happens when the last read guard is dropped. And then if it's, and then we're really going to do like, I guess an R hand, no, we'll just do it this way. This is just going to return. So we're just going to give out. Oh, what am I doing wrong here? Because we know that a previous enter has happened, we know that we don't need to bump the epic. The epic is already locked. So we can just read the pointer cast it to a reference. If that or if the pointer is none, then we just return none. And if that point, this shouldn't be possible. In that case, we didn't give out a guard, so no guard was dropped. So this should be unreachable. If pointer is null, no read guard should have been issued. Because if you look down here, it's only really here that we're going to do self.enters is self.enters get plus one set enters. So it's only here that we increment. We're not going to increment in the else clause. In this case, we're going to do so we're going to do the same thing. Enters is self enters get. If enters not equals zero, and then we're going to enters set enters plus one. And the epic here is just my epic. I'm pretty sure let epic is read after it's incremented. So that's what's set there. So this is just self my epic get. That means this is self plus one set epic. Great. It is funny that the struct has four fields that all say epic in it. I can't use fetch add because this is a cell. I don't think cell has fetch add. It's not as far somewhere. Okay, so what we're missing now is what we're missing now is that the guard, when you drop a guard, I think really I want this to be a reference to self. That has to be a reference to self. The guard is going to hold a reference to Oh, this is why I didn't want to do that balls. Can't I don't want to have to give the type of the handle as an additional generic parameter to the guard, right? Because the guard has the guard T is the generic type parameter of the currently guarded value, which might be different from the overall type of the handle. We can get around this though, by only giving it this information it needs read handle state, which is going to take this and it's going to hold a handle. That's awful. It's going to hold an epic. I guess it's like shared handle, own handle. So stupid. And it needs a reference to the enters. And this is going to now have a pub super handle, just a read handle state. And crucially, the only thing it's really leaving out is the actual pointer type, which is what gives read handle its T. Remember, we're going to do impulse, oops, nope, and do impulse from read, from read handle T or read handle state, just to make it a little bit nicer to use earns a self. This is why the mute version is nicer. This should also not be called handle, this should be called epic. And the own epic is going to be my epic. And the enters is going to be our a enters. That's going to be super. And I guess this old derived debug. Sure, why not. And that actually works. Great. This I guess is probably both copy and clone. Crucially, the drop is going to have to be let enters is self dot handle dot enters dot get minus one. And then handle dot enters dot set enters. If enters is zero, then we're going to do this, which is handle dot shared epic dot store is going to be self dot This is also not right. I feel like we need to update our own epic as well here. Is there a benefit to using cell u 64 over atomic u 64? Yeah, it avoids atomics faster. All right, let's the we certainly have to update the shared epic is going to be handle own epic. Oh, my God, that epic is self dot handle dot own epic dot get. We are the last guard to be dropped. Now reset release our epic. Um, so this should now be, I forget what it's called, is going to be guard read. This needs to be pub super read handle state from self and same thing down here. Um, I remember why I even have my epic. It's because my epic is the current epic with the high bit never set. So, um, yeah, so the implementation considers the epic number just be a number that keeps incrementing by one. It doesn't actually care about even odd. And then it's using the high bit is either a set or not set for whether it's idle or active, I guess. So my epic here, the cell only ever holds the current like round, which only ever goes up by one. And so this is why when the guard gets dropped, we don't actually change the cell u 64, the, the, the my epic, because that one should only be incremented when you increase it. All we have to do is reset the idle bit. Um, this could probably be made nicer by just having it be even odd, but no, it's fine. Um, so crucially what this enables us to do is that all, uh, did I already do that? Right. Is that in, in eb map read? Yeah. Which also means that in read ref, I thought, I guess not. Right. Now we're going to get, now we're going to get a ton of warnings about the mutable stuff again. So let's just go ahead and get rid of all those. We do still have to do the read change to be enter. All right. Where else, where else does this show up? Test quick. Uh, nice. It is a lot nicer when reads can actually be reads. I'll still keep this the way it was because that seems nice. Um, what else do we got? Right. Uh, ev map read ev map map read read ref. Something's not right. Read RS. 243. This doesn't need to be. Ah, um, all right. So cargo check. Just check the lib for me, please. I guess cargo test lib. Uh, so right, right. So that can go away. That makes me happy. I go test. This is a really annoying. I'm pretty sure this is a really annoying issue I think in cargo where notice that the tests still require R to be mutable. Um, and that's because it relies on the, the previous build of ev map. I don't quite know why that is. Why does that, that's real annoying. So I guess the only way I know of to fix this is either to clear your cash or to increment the version of the thing you're building. Um, so, which basically just forces a rebuild of everything still doesn't quite work. Huh? Okay. Ev map tests. Oh, ev map cargo. This is going to have to use version zero nine one. Um, really? Why? I clearly missed the mute self somewhere. But where is the real question? Um, well, we shouldn't need the DRF mute anymore. We can still provide it, but it's, it's less interesting. But why on earth am I blind? I am blind. Oh, ev map read. That's, I'm dumb. There we go. All right. So I've maybe lied, but it is a problem I've seen before, but I'm guessing it probably did not happen this time. Ooh, ev map, right, DRF mute. We no longer need to import that, which also means in left, right, right, we also don't need to import that. Let's see if the test pass again. Fantastic. Okay. So now, um, in theory, the diff we have now should be fairly small. Um, that we can ignore. Uh, oh, did I not change that to not be alpha two anymore? I mean, I'll need to eventually, but um, cargo T doc, those should all be giving warnings, which I'm guessing are silenced because they no longer need to be mutable. What about here? What we got here? Do you have any examples that we need to update here? No, I don't think so. All right. I think we're, I think we're just about there. Yeah, that just should from read to enter. That's fine. Uh, I guess I should bring back the text about, um, read handle not being sync, sort of slip somewhere. I guess really it should go in the documentation for, um, really it should go in the documentation for left, right. Like, I'm going to have to write documentation for left, right anyway. I just haven't done so yet. Um, and much of that is probably just going to get like cribbed from, uh, cribbed from EV map proper. That's something I don't think I'm going to do on stream because my voice is getting horse. Um, but it should also be fairly uninteresting. Like it's just moving text around. Um, Operation now takes M, which is a little sad, but not too bad. Um, I think we simplified the types that he's amount. We removed the need for, oh, we do need to bring back factory now because read guard is no longer sync. Um, that's a little awkward. Yeah, I'm going to have to bring back read guard factory, which is pretty sad. Not even sure where I would put that to be honest. I mean, read guard factory is the other reason why I want it to be, why I sort of like it to be sync, but I also agree that it's nicer for it to not take mute self. I don't know. Read guard factory is pretty simple though. It's a bunch of like mechanical restoration of what we had. Um, I'm going to leave that alone. Um, this stuff didn't really change. I'm really just scanning through to see what, um, whether I missed any like mute arts or anything, but I don't think so. Yeah, this change to enter, but that's mostly uninteresting. That seems fine. That seems fine. All right. I think we're good. I think that's all I wanted to do for today. I'll take some time, uh, probably offline to like, tighten this up and like right up the docks and release left, right, as a crate, but this suggests that this actually works. Um, that this approach of having a generic left, right wrapper type works. Um, and what's neat is that once I land this, you'll be able to use left, right to provide your own, uh, types that you want really fast read concurrency for. Um, but you're willing to say that there's a single writer who I need to go eat some food and drink some water. Um, thanks everyone for coming out. I hope it was interesting. I know is it ended up being a pretty long stream. Um, but that was sort of the intention, right? For this to be a like serious live coding stream again. Um, I see we still have a decent number of people with us. No benchmark. That's true. But remember like benchmark wise, there's no reason to believe that this would change anything. Um, because we haven't changed the algorithm, right? The algorithm is the same. Uh, the only reason why anything would slow down would be because of like, like generics not being monomorphized, but generics should be monomorphized. Um, it would be like crossing crate boundaries, which could matter. Uh, in terms of EV map specifically, I would say it probably doesn't matter because the concern is about the scalability of reads. If each read is a little slower, that doesn't bother me. Hopefully it isn't, but that doesn't bother me. What bothers me is the curve. If we're no longer linear, like if the read performance is no longer linear in the number of readers, that would be a huge problem. Um, but because we haven't changed the algorithm, that shouldn't make a difference. I will point some benchmarks at some point, but um, you could maybe use Bustle to benchmark this. You could try. I don't know. Give it a try. Um, I'll probably post this as like a work in progress PR so people can look at it, uh, put it on GitHub somewhere. Um, but apart from that, I think, I think we're done for today. Thanks for coming out. Hope you had fun. Hope it was interesting and I'll see you next time, probably in like a few weeks. Bye everyone.