Not sure it's worth doing, but here is the idea.
Since there are bytecode values not used by the standard Spin2 interpreter,
the Spin2 compiler could have the ability to automatically allocate them to frequently called user-defined PASM2.
Who decides what is needed frequently enough?
(1) For starters, it would be enough to allow the user to annotate a PASM2 procedure with some directive meaning "make it a bytecode".
The programmer is responsible for using the bytecode calling conventions (aspects like use of PA, PB, return value on top of data stack --- I guess).
Not sure it's worth the trouble, things get complicated:
How would be parameters handled?
Is the top of data stack variable (named 'x' in current Spin2 implementation, if I remember correctly) accessible?
Then the user-defined PASM2 would be required to push when returning a value, unless there was exactly one parameter, stored in 'x'.
Stack state, and interpreter's integrity in general, are in danger.
(2) Detecting most frequently called user-defined procedures with PASM2 body and making them callable as a bytecode seems nice, but assumes too much about the PASM2 code (might not use the bytecode calling conventions).
This is about most frequent static calls, not profile-driven optimization.
It's optimizing for size (of generated Spin2 bytecode) first; some speed improvement is expected but not the focus.
In any case, this needs to be automatic allocation so the programmer cannot change by mistake the meaning of an existing (standard) bytecode value.
It could be a problem if multiple Spin objects want different extensions.
Combining extensions is definitely possible but harder; (2) above ("Detecting most frequently called user-defined [...]") seems the best approach for this given the very limited bytecode space and Cog/LUT memory.
This begins to look like an optimization in the linking phase for building an embedded system image (but not exactly, since the compiled bytecode will need adjustment).