For the past few generations, the interpreter and compiler for Retro have been built around a concept I refer to as a function class. Specifically, the behavior of the system is determined by functions that handle the named symbols in the dictionary as they are encountered. For instance, things that are always run when encountered are handled by one function. Things that get compiled during a definition, but run when interpreting are handled by a separate function. And so forth.
This is a little different than in a classical Forth approach. A classical Forth will have a bit field in the header, with a bit for "immediate" functions, and maybe others for "compile only" or other special cases. Retro instead has a pointer to the class handler function for the entry.
When a function is found, a pointer to its definition (xt) is pushed to the stack. Then a pointer to the class handler is pushed. And finally the class handler is invoked (normally via withClass). The handler will look at the xt on the stack, and any internal system states it cares about, then do the proper action with the xt.
So a simple interpreter-only system basically just a simple class handler something like:
: .always-run( Xt - )
If we want to allow for compiling, this is easy enough to add. We could simply add a compiler variable to tell us if the compiler is on or off and then do:
: .compile-or-run( Xt - )
compiler @ [ , ] [ do ] if ;
There are no defined limits on the number of possible class handlers, or the amount of state they can process. We could easily define class handlers for different data types, functions that can only be invoked at compile time, optimizing definitions, inlining functions, or other behaviors with ease.
This has many benefits to me. It keeps the main processing loop simple, allows easy expansion and customization going forward, and neatly folds a lotion functionality into a simple conceptual model.