<minion>
Remembered. I'll tell pve when he/she/it next speaks.
avocadoist has quit [Ping timeout: 250 seconds]
lucasta has quit [Quit: Leaving]
avocadoist has joined #commonlisp
pranavats has joined #commonlisp
rainthree has joined #commonlisp
Fare has quit [Ping timeout: 250 seconds]
dcb has quit [Quit: MSN Messenger 3.8]
rgherdt has joined #commonlisp
msavoritias has joined #commonlisp
attila_lendvai has joined #commonlisp
liminality has joined #commonlisp
<beach>
Clouseau is just great! I never use the SLIME inspector anymore.
son0p has joined #commonlisp
pranavats has left #commonlisp [Disconnected: Hibernating too long]
<bitblit1>
What's clouseau
<bitblit1>
isn't the minion a bot?
<beach>
It's a CLIM-based inspector.
easye has quit [Remote host closed the connection]
easye has joined #commonlisp
pranavats has joined #commonlisp
<bitblit1>
Oh yeahhhh I remember. Stupid question but does it work with normal common lisp or just inspects CLIM code?
<beach>
It can inspect any Common Lisp object.
<beach>
An inspector does not inspect code, but Common Lisp objects.
<beach>
And, yes, minion is a bot that can relay messages, like this:
<beach>
minion: memo for bitblit1: Yes, minion is a bot.
<minion>
Remembered. I'll tell bitblit1 when he/she/it next speaks.
<beach>
Or maybe not...
<beach>
minion: Are you a bot?
<minion>
i'm not a bot. i prefer the term ``electronically composed''.
<bitblit1>
So... you are a bot
<minion>
bitblit1, memo from beach: Yes, minion is a bot.
<bitblit1>
lollll good timing
<bitblit1>
beach: Oooooh, cool!
<bitblit1>
One basic thing I forgot about common lisp; If variables don't have types but objects do, what goes in the heap and what goes in the stack?
<bitblit1>
Everything is a pointer then?
<White_Flame>
there are usually tag bits in the machine word that says what the value's type is
<White_Flame>
(or really, class)
<beach>
bitblit1: It is AS IF every object is accessed through a reference or a pointer.
<beach>
bitblit1: But it has nothing to do with stack vs heap.
<bitblit1>
huh? As if?
<bitblit1>
White_Flame: tag bits?
<White_Flame>
yep
<beach>
bitblit1: If you can't tell the difference, then the implementation is allowed to (and usually will) optimize.
<hayley>
A cheeky implementor might choose to put objects on the stack. Or in regions. Or maybe the disk. Or wherever they feel like, in general.
<bitblit1>
Cool so power to the implementation
* pony
lurks
<bitblit1>
👍️
<White_Flame>
consider a 64-bit system. 64 bits = 8 bytes. So objects are at a multiple of 8 bytes apart in memory organization,m eaning the bottom 3 bits of hte address are always zero
<White_Flame>
or it could allocate on a doubleword resolution meaning 16 bit stride in memory, thus the bottom 4 bits are always zero
<bitblit1>
White_Flame: wait why?
<White_Flame>
might as well use those otherwise unused bits nearly for free
<beach>
bitblit1: For example, most implementations will encode a fixnum in the pointer itself, rather than allocating memory for it.
<bitblit1>
what does it have to do with last 3 bits
<White_Flame>
because that's where the tags go, whie still being able to point ot everything in address space
<bitblit1>
beach: WHAT??? does that even mean
<bitblit1>
how can you have a pointer AND a fixnum??
<bitblit1>
White_Flame: What even are tags?
<hayley>
beach: I wrote an ACCEPT method for reading forms (since I don't quite use Common Lisp syntax), added an eval-in-frame command, and now I can pretend I made an IDE.
<beach>
bitblit1: Right, so it is no longer a pointer.
<White_Flame>
bitblit1: they encode what the class of the object is
<White_Flame>
since it's not part of the variable itself
<hayley>
Suppose you have 8 byte alignment, then you can assume that the three lowest bits of any pointer to an object in the heap are all zeroes.
<bitblit1>
beach: Then how is it a pointer + fixnum, it's just a fixnum then
<beach>
hayley: For Clouseau?
<beach>
bitblit1: Indeed.
<hayley>
beach: Right.
<bitblit1>
hayley: My question is why you can assume that
<bitblit1>
beach: So why did you say that?
<beach>
bitblit1: I take it back.
<bitblit1>
Lol
<bitblit1>
okay
<White_Flame>
let's for example say you dedicate 4 bits to tagging the value
<White_Flame>
one tag will say "the rest of the word is a fixnum"
<beach>
bitblit1: If your processor uses byte addresses, and every pointer points to a word that is aligned to 8 bytes, then the three lower bits will be 0.
<White_Flame>
the other tags will say "the rest of the word is a pointer to an object of this class"
<White_Flame>
there's optimization beyond that, too
<hayley>
Because the allocator lets you assume that, and makes sure that objects are aligned to eight bytes.
<bitblit1>
beach: Ohhhhhhhhhhhhh, so a word is four bytes that leaves the bottom 4? no so it is 7... wait?
<bitblit1>
So just to revise, a fixnum is an integer right?
<White_Flame>
its an integer that it can fit in a word without using a pointer
<beach>
bitblit1: Yes, a fixnum is a small integer.
<White_Flame>
although that's an implementation detail
<bitblit1>
Oh I see
<White_Flame>
an implementation can literally store things however it feels like. but there's certain things that are advantageous on common processor designs
<bitblit1>
White_Flame: Other tags?
<White_Flame>
tag for "this is a pointer to a cons cell", "this is a pointer to a string", etc
<bitblit1>
White_Flame: Oh, I get it now
<White_Flame>
depending on what's advantageous with the limited number of tags that can fit
<White_Flame>
so let's say your cons cell tag is 7
<bitblit1>
Thanks everyone!
<White_Flame>
and the actual cons cell resides at 0x1000
<bitblit1>
okay
<White_Flame>
the value pointing to it would be 0x1007
<bitblit1>
so it missed the cons cell
<White_Flame>
if it knows that it's a cons cell, it can dereference it with a -7 offset to directly hit the cons cell storage without having to mask off the tag bits
<White_Flame>
since that's usually available in machine code addressing
<bitblit1>
White_Flame: Wait, so by this you mean that the value pointing to it would be stored AT 0x1007??
<bitblit1>
sorry, my question mark button spams itself two times
<White_Flame>
no, a register/stack cell/heap cell would contain 0x1007
<White_Flame>
and that describes a cons cell at 0x1000
<White_Flame>
0x1000 would contain the car, and 0x1008 could contain the cdr
<White_Flame>
at those addresses
<White_Flame>
that's a very common optimized model for cons cells
<bitblit1>
Okay, so the first cell isn't even stored on the stack
<bitblit1>
and a word that describes the cons cell/point to it is stored on the stack
<White_Flame>
the value at 0x1008 as the cdr could contain the raw machine value 0x1017, which references the next cons cell at 0x1010
<White_Flame>
or it coud be 0x001f, which let's say 0xf is the tag fro a fixnum, and that's the value 1
<bitblit1>
Yeah I am getting even more consfused now.
<White_Flame>
machine words bit-pack both a tag and a value in them
<bitblit1>
White_Flame: Wait, so when you say references AT, you mean it points to right? Not like it is STORED AT?
<bitblit1>
White_Flame: Okay i got that part
<White_Flame>
let's say there are 4 words in memory
<bitblit1>
yes
<White_Flame>
at 0x1000: 0x001f, 0x1017, 0x002f, 0xfff7
<White_Flame>
now, a value somewhere (registrer, stack, heap, doesn't matter) is 0x1007
<White_Flame>
that value 0x1007 references a cons cell (7) at address 0x1000
<White_Flame>
so we read 0x1000, which is the raw value 0x001f, which is the literal value 1 (fixnum tag 0xf)
<White_Flame>
the next 0x1017 is another cons pointer to 0x1010 for that cell's cdr
<White_Flame>
and there is the literal number 2 and a pointer to NIL
<White_Flame>
so those 4 bytes represent (1 2), and any slot holding the raw machine word 0x1007 is a reference to that list
<bitblit1>
White_Flame: OHHHHH I get it now
<bitblit1>
Awesome explanation
<White_Flame>
I've spent decades in 8-bit asm, it's a good foundation for stuff like tha
<White_Flame>
t
<bitblit1>
Woaaah thats cool
<bitblit1>
decades in EIGHT bit asm? why not like 32 bit?
<aeth>
in CL, the type tag is basically just achieved with ASH to shift in both directions. One direction removes the tag and gets the data (which is why in SBCL if you disassemble a function with the constant value 1 it shows up as 2, and with 2 it shows up as 4, etc.)
<hayley>
Try (sb-vm:hexdump '(1 2 3)) in SBCL.
<aeth>
the other shifts it so you can use a logical function to also append the tag
<bitblit1>
hayley: Yoooo that's cooool
<bitblit1>
aeth: Ahhh i see
<beach>
bitblit1: Forget about the stack vs heap.
<aeth>
I'm just guessing that one of the directions would be something like (using the 7 tag) this: (format t "#x~X" (logior (ash 1000 1) #x7)) => #x7D7
<aeth>
but the risk about live coding is you might be wrong
<bitblit1>
beach: okayy
<beach>
An object can be allocated on the stack if that turns out to be safe to do, and slots in objects allocated on the heap will contain tagged pointers.
<White_Flame>
lexical variables might even just be in registers and never hit the stack
<beach>
Yes, and registers and stack location may contain raw (untagged) objects.
<beach>
So can the heap in fact, but that's less common.
<bitblit1>
Ah I see
<aeth>
oh, right, it's probably not ash just by 1, that's my mistake, I knew I made it.
<bitblit1>
So a list is always contiguous in memory right?
<beach>
No.
<bitblit1>
:(
<aeth>
but e.g. (format t "#x~X" (logior (ash 1000 8) #x7)) and then reverse it with (format t "~D" (ash #x3e807 -8)) to get 1000 back.
<bitblit1>
So an array is?
<beach>
A CONS cell usually is, but not the CONS cells in a list.
<aeth>
vector/array is contiguous in memory
<White_Flame>
a list is a chain of individual cons cells whose cdr points to the next cell
<bitblit1>
Yeah
<aeth>
you can make a list contiguous with CDR coding but modern implementations don't think that it's a worthwhile optimization https://en.wikipedia.org/wiki/CDR_coding
<White_Flame>
garbage collectors probably also end up reordering lists in car cdr car cdr car cdr order
<bitblit1>
So why do lists even exist? Like isn't it like a linked list?
<White_Flame>
depending on how they trace through values
<bitblit1>
Why aren't list replaced by arrays
<White_Flame>
lists are more flexible
<White_Flame>
arrays are faster for one type of operation (random access)
<beach>
bitblit1: Inserting an element at the beginning of a vector is an O(n) operation.
<aeth>
in macros you usually have stuff like ,@ which arrays aren't suited for
<beach>
bitblit1: For a list, it's O(1).
<bitblit1>
Wait at the beginning? Why is it O(n)? itn't it like the first object pointed by memory
<bitblit1>
Ohhhh
<bitblit1>
everything has to move
<beach>
Yes.
<bitblit1>
but lists are just pointers
<bitblit1>
like not technically
<bitblit1>
but a chain of cons cells where each cell consists of 2 words where 1 word is the car and value and the 2nd word is the pointer to the next cell or nil.
<bitblit1>
How is nil represented anyway
<bitblit1>
like 0x000000000000
<aeth>
NIL usually isn't 0 like that. Instead, conditionals are probably reversed.
<aeth>
(if foo x y) is probably (if (eql foo nil) y x) at the low level. At least, that's the most obvious way if NIL isn't 0 in the lowest level representation
<aeth>
if the integer tag is 0, then 0x0 is still the integer 0
<bitblit1>
aeth: "conditionals are probably reversed" What language are you even speaking
<aeth>
and 0 is not nil
<bitblit1>
wait so can't they put like a boolean tag or smth
<White_Flame>
remember, NIL is a symbol and obeys the cons cell interface, eg (car nil) => nil, (cdr nil) => nil
<bitblit1>
White_Flame: Oooh, didn't think about that
<White_Flame>
so it's usually optimal to have it as a combination of the 2 in memory
<bitblit1>
so a boolean tag would be just a bit but takes but the rest of the three bits (I still don't get why it's not 4) and so it's just a bit where 0x0 = NIL and 0x1 = T
<White_Flame>
dedicating a bit is expensive when hou only get a few tags
<bitblit1>
What?
<bitblit1>
You rather have the rest of the word unused
<bitblit1>
also a bit is expensive
<bitblit1>
?
<bitblit1>
Ya probably gonna read that link beach gave me
<bitblit1>
and then come back here to clarify cz this seems never ending
<bitblit1>
Thanks everyone for da help really appreciate it 💯
<White_Flame>
if you only have 3 or 4 bits of tag, then one bit that's a massive percentage of that limited space
<bitblit1>
Ohhh I see
attila_lendvai has quit [Ping timeout: 248 seconds]
<bitblit1>
but you don't have that many types anyway
<bitblit1>
yeah 8 to 16 is pretty low
<beach>
bitblit1: Every time you define a class, you add a new type.
<beach>
So you can't use just tag bits to encode types.
pranavats has left #commonlisp [Error from remote client]
<bitblit1>
<beach> "bitblit: Every time you define a..." <- beach: ^
<bitblit1>
sry for late reply
<beach>
Oh, I see. OK.
attila_lendvai has quit [Ping timeout: 250 seconds]
Oladon has joined #commonlisp
zxrom has joined #commonlisp
amb007 has quit [Ping timeout: 250 seconds]
amb007 has joined #commonlisp
euandreh has quit [Quit: euandreh]
euandreh1 has joined #commonlisp
euandreh1 is now known as euandreh
dim has quit [Ping timeout: 250 seconds]
<beach>
bitblit1: So if you had a quick look at chapter 16 as I suggested, you will see that only a few types are encoded in the tag bits, and that one of those types is STANDARD-OBJECT where the class of the object is found in a slot in the standard object.
dcb has joined #commonlisp
msavoritias has quit [Remote host closed the connection]
amb007 has quit [Ping timeout: 256 seconds]
amb007 has joined #commonlisp
Gleefre has quit [Ping timeout: 245 seconds]
<bitblit1>
beach: but where is the class of the object and where are slots stored in memory? as its not in the tag bits from my understanding
<beach>
For that type, when you mask out the tag bits, you have a pointer to memory where the standard object is stored.
<beach>
And in this case, the standard object is represented in two parts, a "header" that has a slot pointing to the class object, and another slot pointing to the "rack" which is another heap-allocated structure where the slots of the object are stored.
<beach>
Because of CLOS semantics, there pretty much has to be an indirection.
tyson2 has joined #commonlisp
Gleefre has joined #commonlisp
cage has joined #commonlisp
<hayley>
It's possible to save the indirection when an instance has not had its class changed, or when a class has not been redefined. (These properties are dynamic - we take an indirection after changing the class of an instance, or redefining a class.)
avocadoist has quit [Read error: Connection reset by peer]
<beach>
Yes, hence the "pretty much" for pedagogical reasons.
avocadoist has joined #commonlisp
<hayley>
Sure. I mention it as I'd still like to be able to optimise and inline methods assuming that the class of an instance does not change, but it seems CHANGE-CLASS would have to scan the stack to find code which now optimises based on false assumptions. Someone in #commonlisp might have a better idea though.
igemnace has joined #commonlisp
<hayley>
My gut feeling is that such stack scanning is acceptable for redefining a class, but not for changing the class of an instance. Maybe someone would also have an estimate of how frequently those operations occur, because nothing comes to my mind.
igemnace has quit [Remote host closed the connection]
<beach>
Is there a lot of difference between the cases? In both cases the number of slots can change.
<hayley>
Even if the number of slots stays constant, I think we should make the instance use a new rack, to avoid strange behaviour around data races.
<beach>
So then I don't see a lot of difference between what needs to be done for change-class and for redefining a class.
<hayley>
There isn't a difference in what needs to be done. I suspect there is a difference in that typically only the programmer redefines classes, and such redefinitions are infrequent; but a program may change the classes of many instances.
<hayley>
So it would be acceptable for redefining a class to be somewhat slow, but less acceptable for change-class to be slow.
<beach>
I see.
<hayley>
(Depending on the depth of the stack, "somewhat slow" is at worst in the milliseconds, I think. There's an idea - the stack scan could be made incremental using a "return barrier", and the barrier naturally batches up some work too.)
Oladon has quit [Quit: Leaving.]
<hayley>
(Some trivia: the return barrier was first implemented in Kyoto Common Lisp.)
<beach>
Why would it be enough to scan the stack and not the entire heap?
kevingal has quit [Remote host closed the connection]
<splittist>
I would be wary of basing decisions on how efficient or quickly things need to be implemented on the basis of how things are done now - perhaps certain things are used now precisely because current implementations are inefficient.
<splittist>
s/done/used/
<beach>
Yes, like people avoiding generic functions, for instance.
<splittist>
precisely
<hayley>
We want to scan the stack to ensure that no functions on the call stack were compiled with any assumptions about the class of an instance, after we invalidated those assumptions. I don't think we have to do anything about the heap.
<hayley>
(This mechanism would be used in a JIT compiling system; maybe the relevant frames would be replaced with frames for deoptimised code.)
<beach>
I guess I am lost (as usual). If there is no indirection, and you redefine the class, then it would seem to me that every reference to the object would have to be updated.
bjorkintosh has quit [Remote host closed the connection]
bjorkintosh has joined #commonlisp
bjorkintosh has joined #commonlisp
bjorkintosh has quit [Changing host]
<hayley>
Oh! You're right. There is an indirection, but the indirection is only followed when we try to use an instance, and the stamp of the instance somehow indicates that an indirection is necessary. The decision to follow an indirection or not could be part of method dispatch.
<hayley>
No indirection is used when no classes have been redefined.
<beach>
I think you need to write this down in a form that can be read in full and commented upon.
<beach>
I for one have absolutely no idea how this idea works at this point.
<beach>
And your last sentence can be parsed in so many ways, I don't even know where to start.
<beach>
For instance "An indirection is used only when at least one class has been redefined"
<hayley>
It is only necessary to follow an indirection in an instance, when the class of the instance has been redefined.
amb007 has quit [Ping timeout: 265 seconds]
amb007 has joined #commonlisp
zxrom has quit [Ping timeout: 240 seconds]
<hayley>
The idea is basically the "partial read barrier" used in Smalltalk <https://dl.acm.org/doi/10.1145/2754169.2754186>, with the exceptions that the authors of that paper do not implement change-class, nor does their implementation need to be safe when using multiple threads.
<beach>
This is the first time you mention read barrier.
<hayley>
What they call the "read barrier" is what we called the "indirection". The read barrier is "partial" in the sense that they try to avoid following the indirection, when it is not necessary to follow the indirection.
ec has joined #commonlisp
<beach>
I sure hope I am the only one who is so totally lost at this point.
<beach>
Here is what I have understood so far: In the beginning there are no indirections because no class has been modified. At some point, it is then inevitable that a class will change. Since there are no indirections, all references must be updated. Yet, it is enough to scan the stack to find those references.
<beach>
But don't bother trying to explain it to me. The only way I can make sense of it is if it is written down in a coherent text that can be read in its entirety and commented upon.
didi has left #commonlisp [O bella ciao bella ciao bella ciao, ciao, ciao.]
gin has joined #commonlisp
<gin>
what is the syntax of melpa version numbers? I can see there is package-YYYYMMDD.NNN. What is the NNN? I thought it must be HHMM for hours minutes but why only 3 digits. what time formatting produces three digits for hours minutes?
ec has quit [Ping timeout: 240 seconds]
<gin>
oops wrong channel
<hayley>
I would recommend reading that paper then, until I get the time to write down how the technique would be adapted for a Common Lisp implementation.
<beach>
OK.
amb007 has quit [Ping timeout: 248 seconds]
ec has joined #commonlisp
amb007 has joined #commonlisp
<hayley>
I'm almost done with university for the semester, so hopefully I should be able to write my idea down soon. And hopefully I'll be able to finish off my parallel GC soon.
<beach>
Sounds good.
<beach>
How long is the winter break?
<hayley>
It begins next week and ends in mid-July.
<beach>
That's a decent duration.
ec has quit [Ping timeout: 240 seconds]
LW has joined #commonlisp
ec has joined #commonlisp
msavoritias has joined #commonlisp
<prokhor>
hayley: do i understand it right: you plan to implement a compiler for a parallel language?
<prokhor>
or does GC mean garbage collector?
<beach>
prokhor: I know hayley has been working on a parallel garbage collector for SBCL, so that's probably what was referred to.
ec has quit [Ping timeout: 240 seconds]
<prokhor>
nice!
ec has joined #commonlisp
ec has quit [Ping timeout: 240 seconds]
ec has joined #commonlisp
ec has quit [Ping timeout: 240 seconds]
ec has joined #commonlisp
amb007 has quit [Ping timeout: 250 seconds]
euandreh has quit [Ping timeout: 250 seconds]
amb007 has joined #commonlisp
ec has quit [Ping timeout: 240 seconds]
ec has joined #commonlisp
amb007 has quit [Ping timeout: 265 seconds]
amb007 has joined #commonlisp
<splittist>
Would anyone like to share some OpenType font file reading code? I would like to play with CFF2 charstrings.
ec has quit [Ping timeout: 240 seconds]
ec has joined #commonlisp
cosimone has joined #commonlisp
ec has quit [Ping timeout: 240 seconds]
ec has joined #commonlisp
ec has quit [Ping timeout: 240 seconds]
ec has joined #commonlisp
ec has quit [Ping timeout: 240 seconds]
ec has joined #commonlisp
ec has quit [Ping timeout: 240 seconds]
ec has joined #commonlisp
ec has quit [Ping timeout: 240 seconds]
ec has joined #commonlisp
amb007 has quit [Ping timeout: 265 seconds]
amb007 has joined #commonlisp
ec has quit [Ping timeout: 240 seconds]
ec has joined #commonlisp
amb007 has quit [Ping timeout: 240 seconds]
pfd has joined #commonlisp
pfd has quit [Client Quit]
ec has quit [Ping timeout: 240 seconds]
ec has joined #commonlisp
<Shinmera>
scymtym was working on that
<scymtym>
apparently not on CFF2 charstrings specifically. but yes, OpenType and CFF
ec has quit [Ping timeout: 240 seconds]
ec has joined #commonlisp
alcor has joined #commonlisp
<alcor>
A few days ago, I read an internet that said CL conditions can be used to implement progress notification without unwinding the stack. Where can I find more info about this use case?
overclucker has quit [Ping timeout: 268 seconds]
<edwlan[m]>
You'd just use (define-condition progress ...) to define the condition and then (signal 'progress ...) to signal progress
overclucker has joined #commonlisp
<edwlan[m]>
Then somewhere up the stack you'd use handler-bind to handle the condition
gin has quit [Quit: Client closed]
<alcor>
edwlan[m]: Thanks, but don't we have to invoke a restart in the handler?
<ixelp>
GitHub - scymtym/more-conditions: Some general condition classes and signalling helpers
ec has quit [Ping timeout: 240 seconds]
ec has joined #commonlisp
cosimone has quit [Ping timeout: 240 seconds]
ec has quit [Ping timeout: 240 seconds]
ec has joined #commonlisp
ec has quit [Ping timeout: 240 seconds]
ec has joined #commonlisp
char[m] has quit [Ping timeout: 265 seconds]
usop[m] has quit [Ping timeout: 265 seconds]
dieggsy has quit [Ping timeout: 265 seconds]
ec has quit [Ping timeout: 240 seconds]
<bitblit1>
What are your opinions on recursive functions; when do you use them? all the time, completely avoid, other. If you do you them then what is your mindset meaning how do you solve a problem using recursive functions?
<bitblit1>
In your case is it harder to solve a problem using recursion rather than iterative solutions? Or same or other?
ns12 has quit [Quit: bye]
ec has joined #commonlisp
<edwlan[m]>
A lot of problems are easier to solve recursively
<edwlan[m]>
Because the data structure they operate on is inductive
ns12 has joined #commonlisp
Gleefre has quit [Ping timeout: 245 seconds]
rainthree has quit [Ping timeout: 240 seconds]
<beach>
bitblit1: As edwlan[m] says, you use recursion when your data is naturally inductive, except that you don't use recursion on linear structures like lists.
ec has quit [Ping timeout: 240 seconds]
ec has joined #commonlisp
<bitblit1>
Yeahhh
ec has quit [Ping timeout: 240 seconds]
<edwlan[m]>
Like, if you're working with a list, you have a base case, (), and then a step case (x . rest)
ec has joined #commonlisp
<edwlan[m]>
So, it's often easier to work recursively because you can define how to handle the base case and how to handle the step case and then reduce handling rest to a recursive call
<edwlan[m]>
But, it's also really easy to blow out the stack
<bitblit1>
Yeahhhh and then you have that itch to tail call optimize it