Imagine you write the program "(add print) call". Clearly that function takes two integer arguments. Right?
Well, that's only because you're looking at the high-level version. Compile it down and you get AE 1D B0 88. If you run it on two integers, you get the sum, as expected.
But what happens if you run it with a list on the stack? Then it happens to run the program "(arg-min arg-a) call". Why? The arithmetic-decoder just sees some bytes, and it decodes them to match the correct types.
Representing the actual type of functions would require more bits, make programs longer, and so I haven't implemented that. I'm still pondering things to do here.
Beautiful!
See: http://docs.factorcode.org/content/article-inference.html
In practice, all of the unbalanced stack-effects will become trivially balanced at word boundaries & dynamic stack-effects only occur when metaprogramming.
Also, static types don’t necessarily preclude dynamic stack effects, as long as those effects can be characterised somehow by the type system. In my statically typed concatenative language, Kitten, I originally considered a notion of “regular” types that would let you do something like this:
{ :: r -> r End
} :: r End a* -> r [a]
{ 1 2 3 } :: [Int]
In practice that proved to be too much of an implementation headache for not much benefit. Fixed arities also let you avoid sentinel values (as above) and parentheses (as in Lisps).