uLisp uses tagged unions as its exclusive value representation. Is there a particular reason this is the case? There are fairly simple optimisations that can be achieved from there.
As a proof of concept, I’ve hacked 13-bit fixnums into the AVR version. I did this by also moving the type identifiers around a bit, so that their bit patterns won’t intersect with either the fixnums or valid pointers. I’ve gotten this to mostly work, but there is an assumption in the rest of the code that any object
pointer can be dereferenced, and I’m not sure I’ve tracked all those instances down.
In any case, this means that for numbers between -4096 and 4095, uLisp doesn’t have to allocate from the workspace for the number itself, but instead allocates the cons cell and stuffs the number right in the car. Platforms with larger pointers get more bits to play with, so the ARM version of this, for instance, would get 29-bit fixnums.
I haven’t looked closely into other immediate values, but it should be possible to do the same for a bunch of them - including symbols, which would be the biggest win.