Which I talked about in my uh keynote at Array last last year. And actually a keynote that you actually inspired on on this uh podcast. We talked about how I was gonna pull an audible and give a completely different talk, and I did.
ConorI do consider myself uh your biggest source of inspiration. That is Welcome to ADSP the podcast, episode 124, recorded on March 21st, 2023. My name is Connor, and today with my co-host Bryce, we talk about vectorizing std views filter. So is this gonna be a two-part episode?
BryceBecause we're already uh Probably I think it's I think it's at a point where where it's time for us to start looking at some code.
ConorUh we are we doing uh the screen share thing because we got we we've got about it I not yet.
BryceI I you're you're you're not you're not ready yet to see.
ConorI was gonna say we're we're we made the decision to try not to share share code because we're doing it now because we need to, but but um we made the executive decision to never to do it, but we we need to now, so we will.
BryceOf course, dude, it's a it's a programming podcast. We need to be able to look at it.
ConorShould we uh hit the record button and I'll just upload this to YouTube because every single time people comment, why don't you just post this to YouTube? Okay. So if if you're at this point listening to this podcast, uh link in the description to a live stream that'll be published to my code report YouTube channel.
BryceAlright. Let's let's get started here. Would you agree that this code right here is let's increase the font a tiny bit.
ConorControl shift plus. Because clearly this dude doesn't know what he's doing. You gotta hit the uh click the uh there you go. He's figured it out. Look at this guy, principal architect or whatever the hell his title is, and he doesn't know how to increase the font on compiler. That's Mr.
BrycePrincipal Architect Yeah. Would you agree that this code on the left here is an accurate um approximation of what is is going on in a uh and here let's let's change it a little bit. Um it's gonna be fine as it is. Um would you agree that this is an accident uh an accurate approximation of um of what a filter view is doing?
ConorUm I mean if you're encoding the unary predicate to be that it's greater than the floating point number zero. Yes. So my first thought is like absolutely not because you've hard key.
BryceUm all the elements of a range that are greater than zero, and we're setting that we're setting those elements to be zero. Or sorry, sorry, it's other way around. We're filtering all the elements of a range that are um uh uh less than zero, and we're maybe we're setting them to zero.
ConorYeah, and and for I mean, are we gonna explain this? Because the listener has no idea what we're looking at right now. We have a you want to explain it?
BryceYes, you're you're you're gonna you're gonna just you're gonna explain the loop here. You're gonna explain the code.
ConorAll right, his function is called test search, and it's got extern C, no except, a bunch of stuff that should make you sad about C. And a for loop. It's and the the uh two parameters.
BryceThe only reason that it's extern C here is to make it easy to find the function in the assembly output that we're looking at. Okay.
ConorAll right, here we go. Strap in for uh technical uh remaining however many minutes of part one and part two of this uh this live coding exercise. And there's two arguments to this test search function. One is a double star underscore underscore restrict underscore underscore a, and the other one is a size t called n. We then have a for loop from i equals zero up till n, and then we're incrementing, and then we've got a while loop inside this for loop that is hard-coding our unary predicate of trying to basically see if it's greater than zero and keeping it, and while it's greater than zero, it's increment incrementing, so we're sort of doing we've got two different places where we're incrementing, but we're we're incrementing ISO in both cases, so it doesn't really matter. And then anytime uh we find a negative number, we set that number to zero. It's pretty horrific code. It hurts my soul.
BryceThat inner that inner while loop is um uh is the find if that we're doing in a filter view. That that inner while loop is the thing where we're we're going to find the next element that matches the predicate.
ConorYou know what? I th I know where I kind of feel because I'm looking at the the output of Clang not being able to optimize, I just want to like jump, jump ahead to the end and no, no, wait, wait, wait. I'm saying we I've we've solved this problem in array languages because Yeah, no, no, no, don't ruin it, don't ruin it.
BryceI want to ruin it, Bryce. Let me ruin it.
ConorSo, okay, I won't ruin it. So all I'll say is that array languages solve this problem because this API function, a filter function that takes a unary predicate does not exist. And the first time I ever went to an array language and I tried to do a filtering operation, I was like, what the hell? How come I can't filter something? And that's because filtering something in an array language looks nothing like this, and the way they do it is definitely vectorizable. All right, back to you, Bryce.
BryceAnd and and and we are, throughout this exercise, going to get to a much better version of filter in my book.
ConorFor for the purposes of vectorization, we have to qualify your statement, because that's what we're trying to do here.
BryceI think it's just better in general. Uh not just vectorization, for loop optimization in general. Okay, so I have instruct in this guideball instance, I have um uh we're using Clang and I've instructed Clang to emit some optimization remarks, um, which you can do with the uh the dash R pass um flags, um, which are quite useful. And in particular, I've asked it to uh uh to emit some some optimization uh reports um relating to loop optimizations. Um and so there's there there's two different remarks here. Um Connor, would you care to let the listener know what these remarks are?
ConorIt says remark loop not vectorized, colon could not determine number of loop iterations, bracket.
BryceOkay, and which loop is it referring to?
ConorIt's referring to the inner while loop.
BryceRight. And then the second remark says loop not vectorized. Um and that is um uh so so that's also referring to that same loop. Um, but if we if we tell Clang to attempt to uh vectorize the outer loop, um it'll tell us loop not vectorized, the optimizer was unable to perform the requested transformation. Um basically, this is because the way that the Clang loop vectorizer is set up, it doesn't really do this outer loop vectorization. I mean, it can do some like reordering of loops and loop collapsing, but essentially because this this we have this inner while loop that's inhibiting the the vectorization of the entire loop. Um so no vectorization here, and and we have this scalar code on the right here. And uh and and in this sorry, this is the the code on the right is the assembly code. And in this uh this assembly code we have two actual loops. There's there's the outer loop and then there's the inner loop that does this search. Okay. Now um let's look at a different way to write this code.
ConorAlso, in case it's not clear from people that started to watch this video and didn't listen to the prior conversation, the reason why we're have this inner while loop inside the for loop is to mimic the behavior of the views filter in C20. Obviously, yeah, the more reasonable way to write this, if we weren't trying to replicate the semantics or the moral equivalent of of that function, would just be to evaluate like to check have an if statement, is it greater than zero?
BryceThe thing that I just put on the screen.
ConorYeah, exactly.
BryceOkay, so let's talk about the loop that I just put on the screen.
ConorUm, you know, no while loop, just an if statement.
BryceThat that iterates through the indices, so it's a for loop from from zero to n, uh uh that iterates, you know, some some variable i from zero to n, where n is the size of the uh of the input range. And then the body of this for loop it does if um uh i is greater than uh zero, although I think based on what we said before that should be less than zero, um then uh AI then assign AI to um zero.
ConorYep.
BryceOkay, so so let's talk about how how what happens with this test reference loop. So um for this one, the compiler tells us that it was able to vectorize this loop. And on the right here, we have some very nice, very compact uh uh assembly that vectorizes this loop. And and it and it uh I've told it to generate um output for the uh uh KM architecture, one of the the Xeon Fi architectures that is now defunct, but has it's an AVX512 platform. And so what we see on the right here is in the inner body of this loop, there's four vectorize compare and in um instructions and then four vectorize move instructions. So not only does the compiler vectorize this loop, but it unrolls the loop a little bit, um uh which is uh which is pretty nice. Okay. So this code can be vectorized very nicely. Now we'll we'll put in the um Does it matter that we're just zeroing these out and not erasing them and creating like a newly sized the the re the reason that we're doing a mem set operation here um is because this is uh what we we're looking to study um uh how loop optimizers view this code. And so that means we're gonna be staring at a lot of assembly and setting a value to zero is um very few assembly instructions. Doing like anything more complex, like you know, multiplying two values together or anything more complex than this would mean that the generated assembly would be longer. And um when we are looking at five or ten different ways of writing this function, we want the assembly to be something that's uh that's gonna be very short and sweet so that we can read it over and understand it. And importantly, so that we can like like we can see all the assembly for this function on a single screen, and that we can like look at the assembly line by line and understand exactly what's happening. And likewise, that's why we've chosen you know a built-in type double as our data type, and why for the predicate we've chosen another some something that else very simple, which is just comparing whether something's uh uh greater than zero. So and it it doesn't, you know, we we could we could write this code. I I don't think that it that it greatly affects um uh what's happening here. If you if you the original predicate that I'd worked with was um a predicate that was solely based on the uh the index. So doing like i modulo 2. Um and in that one, the compiler can do some even more clever things because it can sort of figure out the stride pattern, that it can figure out that it's every other element. Um and so that does mess a little bit with with the results that you see. So the nice thing about this predicate is that it is a data dependent predicate. So the compiler doesn't know um how many elements are being filtered out. Um and speaking of that, when we look at the the reason that the compiler is unhappy about that innermost while loop um in our first example, the thing that the compiler tells us is loop not vectorized could not determine number of loop iterations. Um and this is an interesting remark. Clang's loop optimizer remarks are not particularly descriptive. Um basically you get like one of three or four different messages, this is one of them. Um just because it tells you could not determine number of loop iterations, that's not necessarily that that's the true meaning of why it couldn't vectorize that loop. Um however, um I have not I've looked at this at many compilers. Um uh with the Intel compiler, with uh NVIDIA's HPC compiler, um, with a few other compilers. Um and while most compilers do have mechanisms for uh uh vectorizing uh loops with unknown bounds, um the problem here is in fact that this inner loop um it it sort of get m gets merged with the outer loop. And um because we don't know um how many uh you know elements we're gonna filter out in between each one, um it does sort of throw off the compiler's ability to figure out um uh the trip count for this loop, and that is what ends up inhibiting vectorization. Um but the reason is really more because of the merger of these two loops. Um okay. So if we go down to the this last example that I've gotten here, um in this example, well why don't you explain what's happening in this one?
ConorThis function called test ranges, same arguments as before, except now we have a variable local called IDXS, short for indexes, and it is the composition of iota starting at zero to and going to n. And then that pipe to views filter with a predicate that's checking whether our value is greater than zero. And after that, we've got a range-based for loop that loops through each of the elements returned to us by indexes and sets each of those values to zero by using it as an index into our original uh array A using the bracket operators.
BryceCool. What would be a better way to write this using range adapters? A way that would vectorize.
ConorI mean, this is uh not a filter, it's a replace if. We don't have a range adapter version of a replace if, but if we had the C26 pipeline operator, we could just pipe that to uh uh you're being a little too clever because you're you're you're thinking you're thinking about um solving this particular problem here, where I want you to think about solving this in the abstract.
BryceSo, like, yes, I know that we're using a memet here. Uh we're doing a mem set of everything that's you know AI or large or or larger than uh than zero. But imagine that our predicate is just some arbitrary function p and the thing that we're doing in the for loop is just some arbitrary function f.
ConorI mean, that then it then you just go to the tr the generic version of replace if which is transform.
BryceBut so at the end of the day, you want to use a filter here, I assume. There's two problems here. Really it boils down to one problem. Um one, we we have this nested loop. Um and two, because of this nested loop, the compiler can't figure out the space that it's iterating through. Um and I think that this problem is sort of inherent to how we designed um the filter view. That it goes from N to N minus M, and you don't know what that minus M is. Now, let's go back up to the thing that does vectorize here, which is the for loop that has a simple if um in its body that says, like, hey, if this element's filtered in and do this thing and otherwise do nothing. Well, what if like we essentially built a filter that worked like that? Or like what if what if we essentially built a um uh uh a protocol um for composing these you know filter and filter like operations um where the the end consumer would agree that hey um whenever I see some uh some like empty um tombstone value, I'm just gonna ignore it. So so like instead of you giving me a sequence of elements, you can give me a sequence of elements or empty values, and I'll just ignore the empty values.
ConorOkay.
BryceSo uh essentially it is more or less what what you uh what you said before. Like, what if we had a thing called uh let's call it filter O. Um and it will uh so it's gonna take an F and it's gonna return a um uh some form of transform here. And okay, so and and this filter O thing is gonna be you pass it in an F and it returns you some um uh some transform view that takes a transformation function that takes a T and returns an optional of T. Um and what it's and it it has the the your predicate bound. And so what it's gonna do is it's gonna do if um uh t then return t and otherwise return and m or return t which will be wrapped into an optional and otherwise return and change it, change that to a turnary expression. Um okay, you want to return okay, so the turn are expression is uh return Ft uh question mark t colon std null null opt. Um it's complaining about uh that. Okay, all right. So so what so what filter O now does is filter O is a range adapter that you give it a function F and um for all of the elements in the underlying range that F is true for, the produced range has an optional with that value. And for all the elements that um uh the filter function returns false for, there's a corresponding empty optional in the um produced range. Do you follow so far?
ConorI understand. I mean, I have qualms with your stating this is a better filter because it's not. I mean it's maybe better for solving your problem, but this is no longer this is no longer a filter. This is exactly like replace if was the first thing I said, but replace if is just a specialization of transform, which is essentially what you have now. And like you now have a shape preserving um operation in the map bound.
BryceAnd shape preserving is exactly the key intuition here that if you if If you preserve the original loop trip count throughout your range transformations, then the code will will be able to be vectorized. That's the key thing. If you want to keep everything shape preserving. Now the pro the problem is that on the consuming side here, in this for loop that consumes this range, previously the range was a range of um of indices, and now it's a range of optional indices. So anywhere that we consume this, we would now have to add some logic that that checks, oh, is this thing does this thing have a value? Um is it an engaged optional? And if so, then I need to get the value, and otherwise I ignore it.
ConorNo, because well, that's not how you solve this problem. This is you're now in optional land, which is a monad, and so I mean, honestly, if anyone's upset by that, don't be upset by that. It's a simple thing, but they're in in Rust and all the functional languages, they've got basically uh what we should have is a range adapter called like transform maybe that will apply your unary operation only if the optional exists and it just does nothing for the null opt case. So like forget that I said monad and monadic operation, but like the point is is like you pipe from the result of filter O a like in Rust I think it's called map maybe or maybe map, but we would call it transform maybe or transform optional, you know, whatever you want to call it, because we call our map transform and we call our uh maybe optional, so transform optional or something like that that that does this. The thing that I like my whole issue with this is that your answer kind of avoids like the problem?
BryceYeah, the yes the high level I would like to avoid the problem.
ConorThe high-level summary is like if you're using a filter, try to find a way to not use a filter.
BryceAnd well, okay, but but wait, I don't know if I committed this code yet.
ConorUm and my point being is that like if you have that sequence that is shape preserved and of optionals now, that there are monadic operations like filter map that gets rid of the options. So like at some point you still might I gotta log into the VPN so I can show you the next uh atrocious thing. So my point being is that at some point you might need to actually destroy the shape, in which case we're back to what we were talking about originally, and there's no solution.
BryceWhy would you need to destroy the shape?
ConorBecause what if you need to end up, what if like what you actually need to do is filter out like filter in the even numbers? And like you you want a vector of those even numbers at the end of the day.
BryceLike Yeah, then like at the very end, then like my point being is if if if you consider a balanced solution is like it if if if I'm feeding this stuff into uh a for each that I want to be vectorized, like I would like that to happen.
ConorRight. So my point being is like while I push the final piece of code I want to show you. Like the array, the array solution is exactly what you're saying, basically. It's that like find a way to express your problem that is easily vectorizable, which involves shape-preserving operations. As soon as you start doing things like filters that actually destroys the shape, then you're much like much less likely to have a accelerated solution. But you didn't actually create a better filter, you created a different algorithm.
BryceUm I created a better filter for like me as a loop optimizer compiler person.
ConorIt's not a filter though, it's a transform.
BryceUm yeah, but the thing is that. Boom. Okay.
ConorAre we gonna name this episode std? We had one called foreach versus transform, and now we're gonna have filter versus transform.
BryceEverything's a transform and you think there's a thing. You can continue using std views filter. I'm fine with that because I can write this function here, optimize range, that I use in my implementation of foreach. And optimize range just takes your range and it checks whether your range uh uh has a base. This version doesn't have the overload for that, but it checks whether your range has a base. And if it does, then it applies optimized range to that base. And then whenever whenever I s whenever I find in the in the chain of your range uh uh adapters, whenever I find a filter view, I can just transform your filter view into my filter view that my fore each knows how to unpack. Which is what I do here.
ConorYeah, but that not doesn't necessarily save you anything. Not in all cases.
BryceWhat do you mean? It doesn't save anything. In the case where it lets me vectorize the loop.
ConorI mean, but in the case where I then need to do an actual filter where I destroy the op the null ops and get rid of them and end up with a vector that just uh contains the optional values and then unwrap them from optionals, you're still gonna end up doing the filter that we didn't do here.
BryceUm so like you're talking about if I'm going if I'm going into a vector?
ConorYeah, like if you're if you're collecting things.
BryceOkay, but I'm not talking about if I'm going into a vector. I'm talking about that, like I want to write a a fast std for each that when I feed it in a range that has a filter view somewhere in it, that my implementation of for each can be vectorized. And all I gotta do is I gotta take your range and go and find all your filter views and any other annoying, caching, uh lazy views like this that are causing me problems, and replace them with the you know, my my quicker version or my version that does the the tombstomine thing. Tombstomine thing.
ConorI agree that this will work. My but point that I'm making is that it only accelerates a certain category of problems. Like of the umbrella, you know, of the of the problems that use filter, the ones that will benefit from this optimization will benefit, but then there are uh whatever percentage of the other ones that are just gonna be as slow as they were before. Which is fine. Which is fine. You're you're accelerating a a substance.
BryceThis lets me this lets me write this sort of code right here. Which I talked about in my uh keynote at array last last year. And actually a keynote that you actually inspired on on this uh podcast. We talked about how I was gonna pull an audible and give a completely different talk, and I did.
ConorI do consider myself uh your biggest source of inspiration. That is Yes.
BryceAnd so what I have here is this is this little library I've been playing around with um called Spaces, and it's got a 4-H, and this 4-H is aware of this new spaces protocol that I came up with, which is a space is essentially a collection of N ranges, um, each of which is uh nested within the next. Um and I can take one of these spaces and I can apply a uh uh a range view to it, or I can apply a range view to just one particular uh axis of it, and this will generate code that will vectorize. It's really quite something. Like this is this is exactly the what I want out of a multidimensional iteration uh paradigm in modern C. It generates efficient code and it's composable. And it's composable using our existing c compositional primitives in this space, specifically range adapters. You you will also note that that my little implementation here has added some magic that uh does the unpacking of the uh of the tuples uh so that I don't have to do the uh uh the uh structured bindings within the body of each of these lambdas. I I've made my peace with not getting that language feature, but uh I'm I'm I'm gonna make all of my algorithms do the std apply sort of thing where if there's a if you if you send it through a uh a uh tuple of things, it's going to invoke your lambda with uh though that tuple of things unpacked.
ConorI still want that language feature.
BryceHere's my Yeah, I mean sure, I do too, but until I until I get it, I'm gonna I'm gonna do this so that I have nice looking.
ConorWe're referring to a 2017 proposal that died or didn't have further work done it called uh what is it called? Generic lambdas with generic lambdas with structured bindings, something like that. Or polymorphic lambdas with structured bindings. Go back to that example though. So here's the thing it says memset diagonal 2D for each filter. Like, why why are we using for each and filter? This is just a replacive.
BryceYou get like But can't but you're you're you're missing you're missing the point.
ConorI'm missing the point because this example doesn't demonstrate the utility of what you're talking about.
BryceLike it it does work, but but that that's that's only because you're not you're not thinking big.
ConorI don't want to have to be creative. I want you to show me a motivating use case and not the degenerate case that could be spelled more elegantly with a more precisely named algorithm called replacive. Uh but like this composition is like I I believe you that like this this in a this shows the enabling of composing filter filter O, the optimized filter with another algorithm for each.
BryceBut uh yeah, so filter O doesn't actually stand for filter optimize, it stands for filter optional. Because it turns things into optionals. I just didn't have a good name for it.
ConorOkay. My point being is that uh this this presentation, your code example here needs a more motivating example.
BryceThese are the things that I want to be able to write. Like what I've got here, like you know, where I've got um filtering.
ConorI think I think Zoom is frozen because I'm still looking at the same 3D for each filter.
BryceThat's your problem. I'm now on my keynote slides from last year.
ConorAre you sure you're sharing your whole screen?
BryceYeah, I'm sure I'm sharing my whole screen. He said as he hits the stop sharing button and we shared the whole screen. But no, I was sharing the whole screen. I was sharing the whole screen.
ConorYou want what?
BryceI was sharing the screen.
ConorOh no, yeah, sorry. So you were saying now I'm looking at your slide now, so what did you want?
BryceLike, like, okay, uh you know, uh an example here would be if I want to take just the interior points of a particular extent and I want to to stride through that extent and only take every other point. Like, like it's not hard to use your imagination and imagine that I would want to combine two different types of like like to do two different types of filters. Or or to do something like drop, take, although those two need a better name. I mean, and to then like want to do a striding there. Listen, Mr.
ConorMr. Listen, Mr. Principal Architect. You never go into a pitch and then you present something and say, listen, uh, Mr. Executive, it's not hard for you to imagine what I'm trying to tell you here.
BryceThis is what it it is, it is it is a like the the if you can't be motivated by the need for compositional primitives to express multidimensional integration, then like I don't know how to help you. It's like that's like saying, like, I don't know why we need to have range adapters that compose together, because we could just we could just call replace it.
ConorWe're we're you're you're barking up the wrong tree. I agree that these compositional primitives are useful. I'm just giving you a hard time for your bad examples, is all I'm doing. And saying that you need to.
BryceMy examples are glorious and generate beautiful assemblies.
ConorFolks, in the if for those of you watching this on YouTube, let's go down to the chat and tell tell Bryce that you agree with me that his for each filter O example is suboptimal.
BryceI I think that my people in the chat, my HBC people will uh will be on my side here, that it's pretty amazing that we're able to write this code. I do have to go fix this to uh to use on extent.
ConorUh oh look, now now he's updating the example. Surprise, surprise.
BryceYeah, well, it's because I've got fervor. Because this one I I can I can actually just do the selection um in the inner in the middlemost loop, and that's what I wanted to do for this example. Um although the odds of this code compiling is, you know, probably not great, because I'm just writing it live here. But that seems like that's probably correct.
ConorOkay, let's I mean, we gotta wrap this up because we're now at the hour point here. But I gotta say, if we switch back to your slides, I'm not fully convinced your filter O thing. How would that get in here? It works in this case, you know? Because the whole point of this I I have I I have I I assure you it does work. But like, do you do you end up so in in your you want you wanna look at what's going on?
BryceNo, no, no, no, no, no, no, no.
ConorAnswer my question. I'm not saying it doesn't compile and it doesn't work. Do you with this filter O sort of technique on your slide deck slideware, do you end up traversing the entire matrix? So like say you you compose some some things in your spaces library that basically creates uh the equivalent of like a mask that indicates which of your indices are you gonna operate on? Once you have this mask, behind the scenes now, what I'm what I'm visualizing is a bunch of optional values and then null-ops, but then based on what you had before, you're you're you're traversing every element now.
BryceSo you're you're I think you are perhaps thinking that um that that the loop that the multidimensional nature of the loop gets collapsed.
ConorAnd that was never the case.
BryceYes. The way that the that the spaces protocol works is that there is a there's a meta function rank that you call in the space that tells you how many ranks, and then any space aware algorithm has to do a nested for loop for each one of those ranks, and you call mdrange. Here it shows md begin and md end, but that was an earlier iteration of the idea. Now it's you just call mdrange. Um like you call md range you know one, md range two, md range uh uh through md range two, uh md range one, md1, md range zero to get the range for each subsequent innermost loop. And you have to feed the value from uh uh from whatever outermost loop you're in to md range. So it it when you're in in you know the the first like nested loop um or the first inner loop, you need to feed it the the wherever your current location is in the outer loop into md range so that you can incrementally build up the indices as you you go down through this loop structure. It's actually it I don't I I hate to toot my own horn, but it's really actually quite pretty. First of all. First of all, I'm pretty happy with that.
ConorI've never heard a more false statement in my life that I hate to toot my own horn. Bryce. That's uh that's this call.
BryceAnd actually, that little fix that I just did uh compiled without uh without any issues. So, you know, that's pretty nice.
ConorLook at that. A principal architect that still knows how to write code.
BryceOkay, so tests may have failed.
ConorIt compiled, but it uh miraculously uh did not work.
BryceUm yeah, we'll figure that out. We'll figure that out.
ConorI mean, so to just circle back is that the reason that this is important and works and sort of uh nullifies what I was upset about earlier is that specifically the use case for filter in this spaces and indices framework is exactly for the kind of stuff that you were showing. You're essentially creating you can think of it as like a Boolean mask of like which elements in this arbitrarily ranked matrix do I want to perform some kind of transformation on or operation on? And because of that this fake filter is actually completely fine because really the filter we were using before that does that kind of nested iteration or uh nested looping that Bryce, as Bryce described it, is not at all what we want to do. This kind of transform and ignore some values is exactly what we want in this use case. Yes. So, like I said, a motivating example is really just all you needed, Bryce.
BryceYeah. Now I just gotta figure out what exactly I did wrong here because I won't be able to sleep until I do.
ConorWell, we're not gonna do that live on the podcast. So thanks for listening, folks. Bryce, anything you wanna wanna say to wrap up this? I think we we might release this as a 65-minute episode because I'm either that or we're gonna make our listeners suffer and we're gonna start Yeah, make them suffer. We're gonna we're gonna start part two of this just like mid-live coding.
BryceNow what did I do wrong?
ConorAll right, I gotta go back to Bryce does not have anything else to say. He's just over-indexing on his ability, uh, his failing uh test cases here.
BryceYeah.
ConorThanks for listening, folks. Um feel free codes on GitHub. Feel free to uh let us know how terrible this format was. Um and next time Bryce tries to share code, I won't let him. I will I'll just put my foot down. I mean, we we decided I gave I gave the APL show podcast the um hard feedback to not live code and not share things and not write on the chalkboard, and then three episodes later, here we are, Bryce breaking our rule. And uh, I'm pretty sure that was absolutely um impenetrable for the listener, probably. If not, let us know. Give us some feedback on this, and uh just really let Bryce know that you were unhappy with the format, so we won't do it again next time. Be sure to check your podcast app or adspthepodcast dot com for links to all of the things that we mentioned in this episode, as well as a link to a GitHub discussion where you can leave comments, questions, or thoughts on the episode. Thanks for listening. We hope you enjoyed and have a great day.